JWT, JSON Web Token anlamına gelir ve kimlik doğrulama değil, bir yetkilendirme mekanizmasıdır. Şimdi bu ikisi arasındaki farkın ne olduğunu bulalım.
Kimlik doğrulama, kullanıcının tam olarak iddia ettiği kişi olduğunun doğrulanmasını sağlayan mekanizmadır. Kullanıcının kullanıcı adı ve şifre sağladığı ve sistemin bunları doğruladığı bir oturum açma işlemidir. Kimlik doğrulama şu soruyu yanıtlıyor: Kullanıcı kim?
Yetkilendirme , kullanıcının belirli bir kaynağa hangi erişim haklarına sahip olduğunun doğrulanmasını sağlayan mekanizmadır. Kullanıcılara bazı roller ve belirli bir rolün sahip olduğu bir dizi izin verme sürecidir. Yetkilendirme şu soruyu yanıtlıyor: Kullanıcı sistemde hangi haklara sahiptir?
Kimlik Doğrulamanın her zaman önce geldiğini ve Yetkilendirmenin ikinci sırada geldiğini anlamak önemlidir. Yani kimliğinizi doğrulamadan izin alamazsınız. Peki en popüler yetkilendirme yöntemleri nelerdir? Web uygulaması için yetkilendirmeyi ele almada iki ana yaklaşım vardır.
Kullanıcıların yetkilendirilmesi için web üzerinde geleneksel bir yaklaşım, çerez tabanlı sunucu tarafı oturumudur. İşlem, bir kullanıcı oturum açtığında ve bir sunucu onun kimliğini doğruladığında başlar. Bundan sonra sunucu, Oturum Kimliğiyle bir oturum oluşturur ve bunu sunucunun belleğinde bir yerde saklar. Sunucu, Oturum Kimliğini istemciye geri gönderir ve istemci, Oturum Kimliğini çerezlerde saklar. Her istek için istemci, isteğin bir parçası olarak bir Oturum Kimliği gönderir ve sunucu, hafızasındaki Oturum Kimliğini ve kullanıcının bu oturumla ilgili izinlerini doğrular.
Bir diğer popüler yaklaşım ise yetkilendirme için token kullanmaktır. İşlem, kullanıcı oturum açma adını ve parolalarını girdiğinde ve istemci sunucuya oturum açma isteği gönderdiğinde benzer şekilde başlar. Sunucu, bir oturum oluşturmak yerine gizli belirteçle imzalanmış bir belirteç oluşturur. Daha sonra sunucu, belirteci istemciye geri gönderir ve istemcinin bunu yerel bir depolama biriminde saklaması gerekir. Oturum tabanlı yaklaşıma benzer şekilde, istemcinin her istek için sunucuya bir belirteç göndermesi gerekir. Ancak sunucu, kullanıcı oturumuna ilişkin herhangi bir ek bilgi saklamaz. Sunucunun, jetonun oluşturulduğundan ve gizli anahtarla imzalandığından beri değişmediğini doğrulaması gerekir.
Oturum tabanlı yetkilendirme yaklaşımı, Siteler Arası İstek Sahteciliği (CSRF) olarak bilinen bir saldırıya karşı savunmasız olabilir. Saldırganın, ödeme yapmak veya şifre değiştirmek gibi amaçlamadığı eylemleri gerçekleştirmek için oturum açtığı bir siteyi işaret etmesi bir tür saldırıdır.
Başka bir şey, oturum tabanlı yetkilendirme yaklaşımı kullanıldığında, istemci ile sunucu arasında durum bilgisi olan bir oturum oluşturulmasıdır. Sorun, bir istemcinin aynı uygulama kapsamındaki farklı sunuculara erişmek istemesi durumunda, bu sunucuların bir oturum durumunu paylaşmak zorunda olmasıdır. Başka bir durumda, oturum farklı olacağından istemcinin her sunucuda yetkilendirilmesi gerekecektir.
Öte yandan token tabanlı yetkilendirme yaklaşımı, oturum verilerinin sunucu tarafında saklanmasını gerektirmez ve birden fazla sunucu arasındaki yetkilendirmeyi kolaylaştırabilir.
Ancak tokenlar yine de bir saldırgan tarafından çalınabilir ve tokenları geçersiz kılmak da zor olabilir. Bu makalede ayrıntıları ve geçersiz kılmanın nasıl ele alınacağını daha ayrıntılı olarak göreceğiz.
JSON Web Token (JWT), taraflar arasında bilgilerin bir JSON nesnesi olarak güvenli bir şekilde iletilmesi için kompakt ve bağımsız bir yol tanımlayan açık bir standarttır. Bu bilgiler dijital olarak imzalandığı için doğrulanabilir ve güvenilir olabilir. JWT'ler bir sır ( HMAC algoritmasıyla) veya RSA veya ECDSA kullanılarak genel/özel anahtar çifti kullanılarak imzalanabilir.
JSON Web Tokenları noktalarla ayrılmış üç bölümden oluşur .
{ "alg": "HS256", "typ": "JWT" }
Başlık genellikle iki bölümden oluşur: belirtecin türü ve kullanılan imzalama algoritması.
{ "sub": "1234567890", "name": "John Doe", "admin": true }
Yük, kullanıcı hakkındaki ifadeler olan talepleri içerir. Yük daha sonra JSON Web Token'ın ikinci bölümünü oluşturmak için Base64Url olarak kodlanır. Talep olarak kullanılan standart alanların açıklamasını burada bulabilirsiniz.
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
İmza kısmını oluşturmak için kodlanmış başlığı, kodlanmış veriyi, bir sırrı ve başlıkta belirtilen algoritmayı alıp imzalamanız gerekiyor.
Belirteç genellikle aşağıdaki gibi görünür:
xxxxx.yyyyy.zzzzz
jwt.io'ya gidebilir ve örnek bir jetonun veya kendinizinkinin hatalarını ayıklayabilirsiniz. Belirtecinizi Kodlanmış alana yapıştırın ve belirteç imzasının Algoritmasını seçin.
Artık JWT'nin nasıl çalıştığına dair teorik bilgiye sahip olduğumuza göre, bunu gerçek hayattaki projelere uygulayabiliriz. Kahve varlığı için CRUD işlemlerini temsil eden basit bir API'ye sahip olduğumuzu varsayalım. Coffee API'yi temsil eden bir ASP.NET Core API projesi oluşturacağız. Bundan sonra JWT oluşturabilecek bir Identity API'yi temsil edecek başka bir ASP.NET Core API projesi oluşturacağız. Gerçek hayatta, Kimlik Doğrulama/Yetkilendirme amacıyla muhtemelen Identity Server veya Okta veya Auth0 kullanırsınız. Ancak JWT'nin nasıl oluşturulacağını göstermek için kendi Kimlik API'mizi oluşturacağız. Identity API tamamlandığında, denetleyicisini çağırabilir ve kullanıcının verilerine göre JWT oluşturabiliriz. Ayrıca Coffee API'yi, her istekte JWT'nin geçirilmesini gerektiren bir yetkilendirme yapılandırmasıyla koruyabiliriz.
Öncelikle Coffee API'yi temsil eden basit bir ASP.NET Core API projesi oluşturacağız. İşte bu projenin yapısı:
Model
klasöründeki Coffee.cs
ile başlayalım. Id
ve Name
özelliklerine sahip basit bir varlıktır.
namespace Hackernoon.Coffee.API.Model; public class Coffee { public int Id { get; set; } public string Name { get; set; } }
API ile çalışırken varlıklarımızı saklamamız gerekiyor. Öyleyse basit bir bellek içi depolamayı tanıtalım. Data
klasöründeki Storage.cs
dosyasında bulunur.
namespace Hackernoon.Coffee.API.Data; public static class Storage { private static readonly List<Model.Coffee> Data = new(); public static List<Model.Coffee> GetAll() { return Data; } public static bool Create(Model.Coffee model) { if (Data.Any(c => c.Id == model.Id || c.Name == model.Name)) return false; Data.Add(new Model.Coffee { Id = model.Id, Name = model.Name }); return true; } public static bool Delete(int id) { if (Data.All(c => c.Id != id)) return false; Data.Remove(Storage.Data.First(c => c.Id == id)); return true; } public static bool Update(Model.Coffee model) { if (Data.All(c => c.Id != model.Id)) return false; Data.First(c => c.Id == model.Id).Name = model.Name; return true; } }
Coffee API'ye gelen istekleri temsil edecek bir sınıfa ihtiyacımız var. Şimdi Contracts
klasöründe CoffeeRequest.cs
dosyasını oluşturalım.
namespace Hackernoon.Coffee.API.Contracts; public class CoffeeRequest { public int Id { get; set; } public string Name { get; set; } }
Tamamlandığında, CoffeeController.cs
kahve varlığı için CRUD işlemlerini temsil eden Controller
klasörüne uygulayabiliriz.
using Hackernoon.Coffee.API.Contracts; using Hackernoon.Coffee.API.Data; using Microsoft.AspNetCore.Mvc; namespace Hackernoon.Coffee.API.Controllers; [Route("coffee")] [ApiController] public class CoffeeController : ControllerBase { [HttpGet] public IList<Model.Coffee> GetAll() { return Storage.GetAll(); } [HttpPost] public IActionResult Create([FromBody]CoffeeRequest request) { var model = new Model.Coffee { Id = request.Id, Name = request.Name }; if (!Storage.Create(model)) return new BadRequestResult(); return new OkResult(); } [HttpDelete] public IActionResult Delete(int id) { if (!Storage.Delete(id)) return new BadRequestResult(); return new OkResult(); } [HttpPut] public IActionResult Update([FromBody] CoffeeRequest request) { var model = new Model.Coffee() { Id = request.Id, Name = request.Name }; if (!Storage.Update(model)) return new BadRequestResult(); return new OkResult(); } }
Coffee API tamamlandı ve projeyi çalıştırıp Swagger UI'yi aşağıdaki gibi görebiliriz:
Identity API'yi temsil eden başka bir ASP.NET Core API projesi oluşturalım. İşte bu projenin yapısı:
Email
ve Password
özelliklerine sahip yeni bir JWT oluşturma isteğini temsil eden Contracts
klasöründeki TokenGenerationRequest.cs
ile başlayalım.
namespace Hackernoon.Identity.API.Contracts; public class TokenGenerationRequest { public string Email { get; set; } public string Password { get; set; } }
Yalnızca JWT oluşturma mantığını temsil eden TokenController.cs
uygulamamız gerekiyor. Ancak bunu yapmadan önce Microsoft.AspNetCore.Authentication.JwtBearer
NuGet paketinin kurulması gerekiyor.
using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Hackernoon.Identity.API.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace Hackernoon.Identity.API.Controllers; [Route("token")] public class TokenController : ControllerBase { private const string SecretKey = "VerySecretAndLongKey-NeedMoreSymbolsHere-123"; private const string Issuer = "IdentityServerIssuer"; private const string Audience = "IdentityServerClient"; private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(20); [HttpPost] public string Create([FromBody]TokenGenerationRequest request) { var claims = new List<Claim> {new Claim(ClaimTypes.Email, request.Email) }; var jwt = new JwtSecurityToken( issuer: Issuer, audience: Audience, claims: claims, expires: DateTime.UtcNow.Add(Lifetime), signingCredentials: CreateSigningCredentials()); return new JwtSecurityTokenHandler().WriteToken(jwt); } private static SigningCredentials CreateSigningCredentials() { return new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)), SecurityAlgorithms.HmacSha256); } }
SecretKey
, Issuer
ve Audience
gibi hassas yapıların yapılandırmada bir yere yerleştirilmesi gerektiğini unutmayın. Sadece bu test projesini basitleştirmek için sabit kodlanmıştır. Lifetime
alanı 20 dakikaya ayarlanmıştır, bu da belirtecin o süre boyunca geçerli olacağı anlamına gelir. Bu parametreyi de yapılandırabilirsiniz.
Artık projeyi çalıştırıp Swagger kullanıcı arayüzünü aşağıdaki gibi görebiliriz:
/token
uç noktasına bir çağrı yapalım ve yeni bir JWT oluşturalım. Aşağıdaki yükü deneyin:
{ "email": "[email protected]", "password": "password" }
Kimlik API'si karşılık gelen JWT'yi oluşturacaktır:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJqb2huLmRvZUBnbWFpbC5jb20iLCJJc0dvdXJtZXQiOiJmYWxzZSIsImV4cCI6MTcwNzc4Mzk4MCwiaXNzIjoiSWRlbnRpdHlTZXJ2ZXJJc3N1ZXIiLCJhdWQiOiJJZGVudGl0eVNlcnZlckNsaWVudCJ9.4odXsbWak1C0uK3Ux-n7f58icYQQwlHjM54OjgMCVPM
Artık Identity API hazır olduğunda ve bize token sağladığında Coffee API'yi yetkilendirme ile koruyabiliriz. Yine Microsoft.AspNetCore.Authentication.JwtBearer
NuGet paketinin kurulması gerekiyor.
Gerekli hizmetleri kimlik doğrulama hizmetleriyle kaydetmemiz gerekiyor. Bir oluşturucu oluşturduktan hemen sonra Program.cs
dosyasına aşağıdaki kodu ekleyin.
var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "IdentityServerIssuer", ValidateAudience = true, ValidAudience = "IdentityServerClient", ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("VerySecretAndLongKey-NeedMoreSymbolsHere-123")), ValidateIssuerSigningKey = true, }; }); builder.Services.AddAuthorization();
Ara katman yazılımındaki sıranın önemli olduğunu hatırlamak önemlidir. AddAuthentication()
yöntemini çağırıp JwtBearerDefaults.AuthenticationScheme
kimlik doğrulama şeması olarak belirterek kimlik doğrulamayı etkinleştiriyoruz. Bearer
değeri içeren bir sabittir.
namespace Microsoft.AspNetCore.Authentication.JwtBearer { /// <summary>Default values used by bearer authentication.</summary> public static class JwtBearerDefaults { /// <summary> /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions /// </summary> public const string AuthenticationScheme = "Bearer"; } }
Yetkilendirme sırasında JWT'nin hangi parametrelerinin doğrulanacağını açıklayan TokenValidationParameters
belirtmemiz gerekiyor. Ayrıca JWT imzasını doğrulamak için Identity API'dekisigningCredentials'a signingCredentials
IssuerSigningKey
belirtiriz. TokenValidationParameters
hakkında daha fazla ayrıntıyı buradan kontrol edin.
Bir sonraki kod parçası, oluşturucuya kimlik doğrulama ve yetkilendirme yeteneklerini etkinleştiren ara yazılım ekler. UseHttpsRedirection()
ve MapControllers()
yöntemleri arasına eklenmelidir.
app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();
Artık Controller veya eylemleri üzerinde Authorize
niteliğini kullanabiliriz. Bu kodun uygulanmasıyla artık CoffeeController
tüm eylemler bir yetkilendirme mekanizmasıyla korunuyor ve isteğin bir parçası olarak JWT'nin gönderilmesi gerekiyor.
[Route("coffee")] [ApiController] [Authorize] public class CoffeeController : ControllerBase { ..
Coffee API'nin herhangi bir uç noktasına çağrı yaparsak, HttpContext.User
hata ayıklayabilir ve JWT'de belirttiğimiz iddialarla doldurulduğunu ve bir Identity
sahip olduğunu görebiliriz. ASP.NET Core'un Yetkilendirmeyi nasıl ele aldığını anlamak önemli bir şeydir.
Yetkilendirme ile Coffee API'yi korumak için harika çalışmalar yaptık. Ancak Coffee API projesini çalıştırırsanız ve Swagger UI'yi açarsanız isteğin bir parçası olarak JWT gönderemezsiniz. Bunu düzeltmek için Program.cs
dosyasını aşağıdaki kodla güncellememiz gerekiyor:
builder.Services.AddSwaggerGen(option => { option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter a valid token", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "Bearer" }); option.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type=ReferenceType.SecurityScheme, Id="Bearer" } }, new string[]{} } }); });
Bundan sonra sağ üst köşede Yetkilendir butonunu görebileceğiz:
Yetkilendir butonuna tıkladığınızda JWT'ye aşağıdaki gibi girebileceksiniz:
Kendinizi Swagger UI'yi kullanmakla sınırlayamazsınız ve API'nin testini Postman aracı aracılığıyla gerçekleştirebilirsiniz. Önce Identity API'nin /token
uç noktasını çağıralım. Payload olarak JSON kullanacağımız için Headers kısmında Content-Type
başlığını application/json
değeriyle belirtmemiz gerekiyor.
Bundan sonra /token
uç noktasını çağırabilir ve yeni bir JWT alabiliriz.
Artık JWT'yi kopyalayıp Coffee API'yi çağırmak için kullanabiliriz. Uç noktaları test etmek, oluşturmak ve güncellemek istiyorsak Identity API'ye benzer Content-Type
başlığını belirtmemiz gerekir. Authorization
başlığının ayrıca Bearer [your JWT value]
değeriyle ayarlanması gerekir. Bundan sonra Gönder düğmesine basın ve sonucu görün.
Hatırlayacağınız gibi JWT'nin yük kısmı, tam olarak anahtar/değer çiftleri olan değerlere sahip bir dizi talepten oluşur. Rol tabanlı yetkilendirme, kullanıcının ait olduğu role bağlı olarak uygulama kaynaklarına erişimi farklılaştırmanıza olanak tanır.
Identity API'deki TokenController.cs
dosyasındaki Create()
metodunu, role yeni bir talep ekleyen kodla güncellersek; Coffee API'de rol tabanlı kimlik doğrulamayı gerçekleştirebiliriz. ClaimTypes.Role
, rol talebinin önceden tanımlanmış adıdır.
var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim(ClaimTypes.Role, "Barista") };
Rol adını belirterek CoffeeController.cs
dosyasındaki Authorize
özelliğini güncelleyin:
[Authorize(Roles = "Barista")]
Artık Coffee API'ye çağrı yapan tüm kullanıcıların Barista
değerine sahip rol talebinde bulunması gerekiyor. Aksi takdirde 403 Forbidden
durum kodu alacaklardır.
Authorize
özelliği, rol tabanlı kimlik doğrulamayı kolayca işleyebilir. Peki ya bu yeterli değilse ve erişimi yaş gibi bazı kullanıcı özelliklerine göre farklılaştırmak istiyorsak? Muhtemelen taleplerinizi JWT'ye ekleyebileceğinizi ve bunları yetkilendirme mantığı oluşturmak için kullanabileceğinizi tahmin etmişsinizdir. Rol tabanlı yetkilendirmenin kendisi, talep tabanlı yetkilendirmenin özel bir durumudur; tıpkı rolün önceden tanımlanmış türdeki aynı talep nesnesi olması gibi.
Identity API'deki TokenController.cs
dosyasındaki Create()
yöntemini yeni bir iddia ekleyen IsGourmet
koduyla güncelleyelim.
var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim("IsGourmet", "true") };
Coffee API'deki Program.cs dosyasında bir iddiayı doğrulayan ve Authorize
özelliğinde kullanılabilecek bir politika oluşturmamız gerekiyor. AddAuthentication()
yöntem çağrısından hemen sonra aşağıdaki kodun eklenmesi gerekir.
builder.Services.AddAuthorization(opts => { opts.AddPolicy("OnlyForGourmet", policy => { policy.RequireClaim("IsGourmet", "true"); }); });
İlke adını belirterek CoffeeController.cs
dosyasındaki Authorize
özniteliğini güncelleyin:
[Authorize(Policy = "OnlyForGourmet")]
Tebrikler! .NET'te JWT'yi öğrenmek için büyük çaba harcadınız. Artık JWT ilkelerini ve .NET uygulamalarında yetkilendirme gerçekleştirmek için onu kullanmanın neden önemli olduğunu iyice anlamış olmanız gerekir. Ancak ASP.NET Core uygulamalarında kimlik doğrulama ve yetkilendirme alanında henüz yeni bir başlangıç yaptık.
Bu makalede tartıştığımız konularla ilgili Microsoft belgelerine bakmanızı öneririm. .NET platformunda yetkilendirme ve rol yönetimi için de birçok yerleşik yetenek bulunmaktadır. Yetkilendirmeyle ilgili Microsoft belgeleri bu makaleye iyi bir katkı olabilir.