Yazılım Mimarileri ve Tasarım Desenleri Üzerine

Asp.NET Core 3.1 ile Token Bazlı Kimlik Doğrulaması ve Refresh Token Kullanımı(JWT)

Merhaba,

Günümüz web uygulamalarında veri güvenliğini sağlayabilmek için kullanıcı doğrulama yöntemleri arasından en etkili ve çağdaş olanlarından kabul edebileceğimiz Token Bazlı Kimlik Doğrulama yöntemi ile sistemsel ve kullanıcı açısından en az maliyette en yüksek verimi elde edebilmekte ve güvenli bir şekilde gerekli authorization işlemlerini gerçekleştirebilmekteyiz.

Bu makalemizde Asp.NET Core uygulamalarında Token Bazlı Kimlik Doğrulama yapılanmasını a’dan z’ye inceleyeceğiz. Esasında daha önceden bu konu üzerine kâh Asp.NET mimarisi için olsun kâh Node.JS için olsun birçok makale karalamış bulunmaktayım. İlgili makaleleri aşağıda listelersek eğer;

Görüldüğü üzere konuya dair bloğumuzda önceden birçok kaynak mevcuttur. Peki hoca, madem konuya dair önceden karaladıkların var. Ne diye tekrar aynı konuda içerik oluşturuyorsun? sorunuzu duyar gibiyim. Biliyorsunuz ki son zamanlarda Asp.NET Core Identity üzerine detaylı yazı dizisi kaleme almaktayım. İşte bu yazı dizisinde authentication yöntemlerinden biri olarak yer alacak ve konuyu sıfırdan ele alıp ve adım adım kodlama standartlarını belirleyecek bir içerik ortaya koyma niyetindeyim. Ayriyetten diğer makalelerde bulamayacağınız terminolojilerle birlikte farklı tanımlamalar ve yöntemlerde ele alınmış olacaktır.

İşte bu niyetle yeni bir minvalde yol alacağız… O halde haydi gelin başlayalım.

Başlarken

Makalemize başlarken bir adet Asp.NET Core Web API projesinin hali hazırda bulunmasıyla birlikte gerekli authentication kontrolünde bizlere eşlik edecek olan aşağıdaki “Users” tablosunu tasarlamamız gerekmektedir.

    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Surname { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }
        public string RefreshToken { get; set; }
        public DateTime? RefreshTokenEndDate { get; set; }
    }

Users tablosunu Code First yahut Database First yaklaşımlarından herhangi biriyle tasarlayıp, oluşturabilirsiniz.

Oluşturulan “Users” tablosuna tekrar göz atarsak eğer standart kolonların dışında “RefreshToken” ve “RefreshTokenEndDate” kolonları mevcuttur. Bu kolonların ne amaçla oluşturulduğuna değinebilmek için öncelikle üretilecek token yapılanmasıyla ilintili “Access Token” ve “Refresh Token” terimlerini açıklamamız gerekmektedir.

  1. Access Token
    OAuth 2.0 protokolüne göre RFC 7519 standartında olan ve authorization neticesinde belirli bir expire süresine bağlı bir şekilde üretilip kullanıcıya sunulan güvenli bir anahtar değeridir. Anlayacağınız token’ın ta kendisidir 🙂
  2. Refresh Token
    Access token’ın expire süresi sona ermeye yaklaştığında veya sona erdiğinde yeni bir access token üretebilmek için kullanılan token değeridir. Kullanıcıya verilen access token değeri yanında refresh token değeri verilerek, access token süresi dolduğu taktirde bu refresh token ile yeni token talebinde bulunabilecektir. Böylece kullanıcı token süresi dolduğu taktirde oturumdan düşürülmeden yeni token elde edebilecek ve yoluna devam edecektir.

Dikkat!!! Her zaman Refresh Token Expiration Access Token Expiration’dan fazla olmalıdır!

İşte “Users” tablosundaki “RefreshToken” kullanıcı için üretilmiş olan refresh token değerini tutacak olan kolondur. “RefreshTokenEndDate” kolonu ise üretilen refresh token değerinin işlev/kullanım süresini belirleyecek olan zaman bilgisini tutan alandır.

Token Servisinin Uygulamaya Eklenmesi

Asp.NET Core uygulamasında token ile authentication işlemlerini gerçekleştirebilmek için JWT Token servisini uygulamaya eklememiz gerekmektedir. Bunun için “Startup.cs” dosyasındaki “ConfigureServices” metodu içerisinde “AddAuthentication” middleware’i aracılığıyla bir şema oluşturarak işe başlamalıyız. İlgili metodu aşağıdaki görselde olduğu gibi çağırarak ikinci overloadına göz atarsak eğer;

görüldüğü üzere bir default scheme değeri istemektedir.
Peki bizden istenen bu default scheme değeri nedir?
Bir authentication işlemini farklı pozisyonlar için farklı şekilde tanımlamak isteyebiliriz.
Örneğin; ‘Bayi Authentication‘ – ‘Kullanıcı Authentication‘ gibi…
İşte bu mantıkta bir isimlendirmeyle farklı authentication şeması tanımlanabilir. Bizler ister benzer şekilde opsiyonel değerler tanımlayarak şema belirleyebilir yahut default şema ayarı vererek buradaki ihtiyaca daha spesifik çözüm getirebiliriz. Eğer ki tercihimiz default şema ayarı ise bunun için “Microsoft.AspNetCore.Authentication.JwtBearer” kütüphanesindeki “JwtBearerDefaults” sınıfını kullanmamız gerekecektir.

Velhasıl, JWT Token servisini uygulamaya entegre edersek;

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(option =>
            {
                option.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateAudience = true,
                    ValidateIssuer = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = Configuration["Token:Issuer"],
                    ValidAudience = Configuration["Token:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Token:SecurityKey"])),
                    ClockSkew = TimeSpan.Zero
                };
            });
            .
            .
            .
            services.AddControllers();
        }
    }

şeklinde çalışma yapmamız yeterli olacaktır. Burada dikkat ederseniz “Audience”, “Issuer”, “LifeTime”, “SigningKey” ve “ClockSkew” kavramları mevcuttur. Bu kavramların ne olduğuna dair bir açıklama yapmamız gerekirse eğer;

Şimdi yönümüzü tekrar yukarıdaki kod bloğuna çevirerek 15 ile 22. satır arasındaki yapılanları inceleyelim;

Ayriyetten yukarıdaki kod bloğunda kullanılan değerler için Configuration propertysi kullanılmaktadır. Yani bu ilgili verileri “appsettings.json” dosyasından okuyoruz anlamına gelmektedir. Haliyle örnek uygulamamızda verileri ilgili dosyada aşağıdaki gibi tuttuğumuzu ifade etmekte fayda var;

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Token": {
    "Issuer": "www.myapi.com",
    "Audience": "www.bilmemne.com",
    "SecurityKey":  "doldur be meyhaneci, boş kalmasın kadehim ..."
  }
}

Token Handler Sınıfını Oluşturma

JWT Token servisini uygulamaya entegre ettikten sonra sıra Token değerini oluşturmaktan sorumlu olan Token Handler sınıfını inşa etmeye geldi.

İlk olarak üretilecek token ve refresh token değerlerini taşıyacak olan “Token” modelimizi tasarlıyoruz.

    public class Token
    {
        public string AccessToken { get; set; }
        public DateTime Expiration { get; set; }
        public string RefreshToken { get; set; }
    }

Ardından Token Handler sınıfımızı inşa edebiliriz;

    public class TokenHandler
    {
        public IConfiguration Configuration { get; set; }
        public TokenHandler(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        //Token üretecek metot.
        public TokenAuthentication.Models.Token CreateAccessToken(User user)
        {
            Models.Token tokenInstance = new Models.Token();

            //Security  Key'in simetriğini alıyoruz.
            SymmetricSecurityKey securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Token:SecurityKey"]));

            //Şifrelenmiş kimliği oluşturuyoruz.
            SigningCredentials signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

            //Oluşturulacak token ayarlarını veriyoruz.
            tokenInstance.Expiration = DateTime.Now.AddMinutes(5);
            JwtSecurityToken securityToken = new JwtSecurityToken(
                issuer: Configuration["Token:Issuer"],
                audience: Configuration["Token:Audience"],
                expires: tokenInstance.Expiration,//Token süresini 5 dk olarak belirliyorum
                notBefore: DateTime.Now,//Token üretildikten ne kadar süre sonra devreye girsin ayarlıyouz.
                signingCredentials: signingCredentials
                );

            //Token oluşturucu sınıfında bir örnek alıyoruz.
            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();

            //Token üretiyoruz.
            tokenInstance.AccessToken = tokenHandler.WriteToken(securityToken);

            //Refresh Token üretiyoruz.
            tokenInstance.RefreshToken = CreateRefreshToken();
            return tokenInstance;
        }

        //Refresh Token üretecek metot.
        public string CreateRefreshToken()
        {
            byte[] number = new byte[32];
            using (RandomNumberGenerator random = RandomNumberGenerator.Create())
            {
                random.GetBytes(number);
                return Convert.ToBase64String(number);
            }
        }
    }

Yukarıdaki kod bloğunu incelerseniz eğer access token ile birlikte refresh token değerlerini üretecek bir sınıf tasarlamış bulunmaktayız. Burada “CreateRefreshToken” metodu içerisinde refresh token değeri için “RandomNumberGenerator” nesnesinin kullanıldığına dikkatinizi çekerim. Alternatif olarak burada isterseniz Guid tipinde bir değer dönebilir yahut farklı bir Token değeri oluşturup refresh token olarak kullanabilirsiniz.

Controller’ların Oluşturulması

İlk olarak kullanıcılardan gelecek olan login istekleri için “UserLogin” view model’ını oluşturalım.

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

Ardından kullanıcılar tarafından yapılacak olan kayıt yahut login isteklerini karşılayacak olan “Login(Controller).cs” dosyasını oluşturalım.

    [ApiController]
    [Route("api/[controller]")]
    public class LoginController : ControllerBase
    {
        readonly TokenExampleContext _context;
        readonly IConfiguration _configuration;
        public LoginController(TokenExampleContext content, IConfiguration configuration)
        {
            _context = content;
            _configuration = configuration;
        }
        [HttpPost("[action]")]
        public async Task<bool> Create([FromForm]User user)
        {
            _context.Users.Add(user);
            await _context.SaveChangesAsync();
            return true;
        }
        [HttpPost("action")]
        public async Task<TokenAuthentication.Models.Token> Login([FromForm]UserLogin userLogin)
        {
            User user = await _context.Users.FirstOrDefaultAsync(x => x.Email == userLogin.Email && x.Password == userLogin.Password);
            if (user != null)
            {
                //Token üretiliyor.
                TokenHandler tokenHandler = new TokenHandler(_configuration);
                TokenAuthentication.Models.Token token = tokenHandler.CreateAccessToken(user);

                //Refresh token Users tablosuna işleniyor.
                user.RefreshToken = token.RefreshToken;
                user.RefreshTokenEndDate = token.Expiration.AddMinutes(3);
                await _context.SaveChangesAsync();

                return token;
            }
            return null;
        }
    }

Authorization işlemlerini test edebilmek için kullanıcı isteklerini karşılayacak “Test(Controller).cs” dosyasını oluşturalım.

    [Authorize]
    [ApiController]
    [Route("api/[controller]")]
    public class TestController : ControllerBase
    {
        public string Index()
        {
            return "Yetkilendirme başarılı...";
        }
    }

Yukarıdaki “Test(Controller).cs” sınıfını incelerseniz eğer “Authorize” attribute’u ile işaretlenerek yetkisiz erişim engellenir bir hale getirilmiştir.

Test Edelim

Sıra oluşturduğumuz yapılanmayı test etmeye gelmiştir. Tüm yapılanmayı Postman vasıtasıyla test edeceğiz.
İlk olarak kullanıcı oluşturalım.

Ardından “Test(Controller)” controller’ına bir istekte bulunalım.

Görüldüğü üzere ilgili istek neticesinde yetkisiz erişim yapmaya çalışıldığına dair “401 Unauthorized” durum kodunu döndürmektedir.

O halde token talebinde bulunalım.

Görüldüğü üzere token talebi neticesinde bizlere access token, expiration ve refresh token verilerini barındıran bir JSON verisi döndürülmüştür. Artık yapmamız gereken ilgili access token değerini kullanıp bir önceki “Test(Controller)” sınıfına istekte bulunmaktır.

Yukarıdaki ekran görüntüsünü incelerseniz eğer “Headers” kategorisinde “Authorization” keyine karşılık verilen “Bearer {token}” değeri ile yapılan istek neticesinde token değerimizin başarılı bir şekilde doğrulandığını görmekteyiz.

Unutmayın!!! Token bazlı kimlik doğrulama senaryosunda sistemin çalışması için “Startup.cs” dosyasındaki “Configure” metodu içerisinde ‘UseAuthorization’ middleware’ından önce ‘UseAuthentication’ middleware’inin çağrılması gerekmektedir. Aksi taktirde token ile yapılan istekler yine -401 Unauthorized- durum kodu olarak geri dönecektir.

Refresh Token Kullanımı

Üretilen token değerinin süresi bitmeden yahut bittiğinde kullanıcıyı yeniden login işlemleriyle meşgul etmeden gerekli yetkilendirmeyi tekrar sağlayabilmek için refresh token mekanizmasını kullanacağız. Bu mekanizma temelde aşağıdaki gibi bir stratejiden ibarettir.

Yukarıdaki görüntüde olduğu gibi client öncelikle gerekli authorization için Authorization Server’a bir istekte bulunuyor ve bu istek neticesinde “Access Token” ile birlikte “Refresh Token” değerlerini elde ediyor. Süreçte Resource Server’a yapılan tüm istekler access token aracılığıyla gerçekleştiriliyor… Taa ki access token’ın süresi bitip geçersiz oluncaya kadar… İşte bu durumda kullanıcıyı tekrardan login sayfasına yönlendirerek kullanıcı adı ve şifre kontrolü yapmak yerine kullanıcıya önceden gönderilmiş olan refresh token değeri ile Authorization Server’a bir istekte daha bulunuluyor ve tekrar yeni bir access token üretilerek hiç vakit kaybetmeksizin kullanıcıya bu yeni token değeri ile kaldığı yerden devam etme olanağı tanınıyor. Tabi bu arada refresh token kullanıldıktan sonra geçerliliğini yitirerek üretilen access token değerine eşlik edebilecek yenisi üretilmiş oluyor.

Bu mantıkta refresh token değerini kullanarak yeni bir access token üretebilmek için gerekli istekleri karşılayacak endpointi aşağıdaki gibi tasarlayabiliriz.

    [ApiController]
    [Route("api/[controller]")]
    public class LoginController : ControllerBase
    {
        readonly TokenExampleContext _context;
        readonly IConfiguration _configuration;
        public LoginController(TokenExampleContext content, IConfiguration configuration)
        {
            _context = content;
            _configuration = configuration;
        }
        .
        .
        .
        [HttpGet("[action]")]
        public async Task<TokenAuthentication.Models.Token> RefreshTokenLogin([FromForm] string refreshToken)
        {
            User user = await _context.Users.FirstOrDefaultAsync(x => x.RefreshToken == refreshToken);
            if (user != null && user?.RefreshTokenEndDate > DateTime.Now)
            {
                TokenHandler tokenHandler = new TokenHandler(_configuration);
                TokenAuthentication.Models.Token token = tokenHandler.CreateAccessToken(user);

                user.RefreshToken = token.RefreshToken;
                user.RefreshTokenEndDate = token.Expiration.AddMinutes(3);
                await _context.SaveChangesAsync();

                return token;
            }
            return null;
        }
    }


Görüldüğü üzere refresh token değeri ile yapılan istek neticesinde yeni bir access token değeri elde etmekte ve bu değeri aşağıdaki görselde olduğu gibi devam faaliyetlerimizde kullanabilmekteyiz.

Evet… Görüldüğü üzere Refresh Token ile birlikte Token Bazlı Yetkilendirme işlemi bu çabadan ibarettir diyebiliriz 🙂 Sabredipte son noktasına kadar okuduğunuz için teşekkür ederim.

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

Not : Örnek projeyi indirmek için buraya tıklayınız.

Exit mobile version