.NET Core API + ASP.NET Identity, JWT Authorization ve Authentication

Samet UCA
10 min readAug 1, 2023

--

Merhabalar. Bu makalemizde .Net Core Web API JWT ile kimlik doğrulama işlemlerini yapacağız. Ancak ilk olarak JWT nedir bundan kısaca bahsedelim.

Görsel Kaynak : jwt.io

Yukarıda gördüğünüz gibi 3 bölümden oluşan bir biletten bahsediyoruz.

Header

Genel olarak jwt türü ve algoritması(RSA, SHA256) hakkında bilgi verir.

Payload

En önemli verilerin tutulduğu teknik olarak güvenceye almak istediklerimizin yer aldığı (tokenin süresi, kullanıcı ait bilgiler gibi) bilgiler içerir.

Signature

Son kısım ise aynı zamanda base64url kodlu olan imzadır. header, payload, key ve son olarak header kısmında belirtilen algoritmanın işlenmiş halidir. Sunucu bu yol ile header veya payload kısmında bir değişilik olup olmadığını ve doğru kişiden mi geldiğini kontrol eder.

Peki JWT Nasıl Çalışır?

Örnek olarak bir e-ticaret sitesine kullanıcı bilgileriniz ile giriş yapacaksınız. İlk adımda bilgileriniz sunucu tarafında kontrol edilir eğer kullanıcı bilgileriniz doğru ise sunucu tarafında tutulan secret-key ile size bir token üretilir. Bu token size gönderilir ve yeni bir sunucu tarafına yapılacak olan istekte sunucuya token ile gidersiniz. Sunucu token’ı public key ile çözer.

Daha iyi anlaşılması için aşağıda ki görseli inceleyebiliriz.

Görsel Kaynak : https://dev.to/ronakvsoni/log-in-with-jwt-authentication-in-rails-and-react-1382

Peki sıfırdan JWT ile yetkilendirme kullanan bir api uygulaması yazarak işe başlayalım.

Projemiz .Net Core Web API projesi olacak. Konunun dışına çıkmamak adına pattern kullanmayacağız. İlk olarak projemizi oluşturalım. Devamında klasörlerimizi oluşturalım.

hemen sonra gerekli paketleri nuget üzerinden kurabiliriz. Paketler dotnet6'a uygun olarak kurulduğundan siz kendi .net sürümünüze uygun olanı seçmelisiniz.

İlk olarak daha sonra tekrar dönmeyelim diye ileride kullanacağımız sınıflarımızı oluşturalım. Uygulamada 3 farklı rol olacak.

  1. Administrator
  2. Moderator
  3. User

Bu rolleri bir enum içerisinde tutacağız. İlgili sınıfı oluşturalım.

public class Authorization
{
public enum Roles
{
Administrator,
Moderator,
User
}
public const string default_username = "user";
public const string default_email = "user@secureapi.com";
public const string default_password = "Pa$$w0rd.";
public const Roles default_role = Roles.User;
}

Görüldüğü üzere default olarak verilen değerler var. Bu değerleri bir sonra ki adımlarda kullanacağız.

Sıra context sınıflarımızı oluşturmaya geldi. 2 adet context sınıfımız olacak. Biliyoruz ki context sınıfları veritabanı ile uygulamamız arasında köprü görevi gören bir yapıdır. İlk olarak bu yapıya ihtiyacımız var.

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options):base(options)
{
}
}

görüldüğü üzere IdentityDbContext base olarak alınmış ve bu sınıf generic. İçerisine ise ApplicationUser sınıfını verdik. Bu sınıf MsIdentity kütüphanesinde custom kullanıcı bilgilerini kullanmamız için gerekli. Yeni bir sınıf oluşturalım ve adını ApplicationUser olarak belirleyelim.

public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

Dikkat ederseniz IdentityUser sınıfından kalıtım alarak bu sınıfı genişlettik. Sadece ek olarak first ve last name bilgilerinide kullanacağımız için gerekli 2 alanı eklemiş bulunuyoruz. Daha fazla field eklenebilir orası size kalmış. Sıra ikinci context sınıfımıza geldi. Bu sınıfımız uygulamamız ayağa kalktığı anda veritabanına default değerler gönderecek. Bir çok projede Seed şeklinde kullanılmaktadır.

Yeni bir sınıf oluşturalım ve adını ApplicationDbContextSeed olarak belirleyelim.

public class ApplicationDbContextSeed
{
public static async Task SeedEssentialAsync(UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager)
{
//Seed roles
await roleManager.CreateAsync(new IdentityRole(Authorization.Roles.Administrator.ToString()));
await roleManager.CreateAsync(new IdentityRole(Authorization.Roles.Moderator.ToString()));
await roleManager.CreateAsync(new IdentityRole(Authorization.Roles.User.ToString()));
//Seed Default User
var defaultUser = new ApplicationUser { UserName =Authorization.default_username,
Email = Authorization.default_email,
EmailConfirmed=true,
PhoneNumberConfirmed = true,
};
if (userManager.Users.All(u=>u.Id!=defaultUser.Id))
{
await userManager.CreateAsync(defaultUser, Authorization.default_password);
await userManager.AddToRoleAsync(defaultUser, Authorization.default_role.ToString());
}
}
}

Bu sınıfta ise, Identity ile kullanıma açık olan UserManager<ApplicationUser> sınıfına biraz önce oluşturduğumuz sınıfı verdik. Aynı zamanda roller ile de çalışacağımız için hazır olarak gelen RoleManager<IdentityRole> sınıfımızı generic olarak veriyoruz. Sonrasında roller ile daha detaylı işlemler yapacağız. Ancak bu adım için yeterli.

Sınıfımızı incelersek; asenkron şekilde 3 farklı rol eklenmekte. Bu rolleri default olarak daha önce oluşturduğumuz sınıfta ki değerleri veriyoruz. Rol ve kullanıcı bilgilerini default olarak setledikten sonra son aşamada veritabanında bu kaydın aynısının var olup olmadığını kontrol ediyor eğer yok ise kaydediyoruz.

if (userManager.Users.All(u=>u.Id!=defaultUser.Id))
{
await userManager.CreateAsync(defaultUser, Authorization.default_password);
await userManager.AddToRoleAsync(defaultUser, Authorization.default_role.ToString());
}

Oluşturduğumuz yapıları Code First yöntemi ile veritabanına yansıtmamız gerekiyor. İlk olarak AppSettings.json dosyasının içerisine aşağıda gördüğünüz alanın size uygun halini yazınız.

"ConnectionStrings": {
"DevConnection": "Data Source=localhost;Initial Catalog=SecureDb;Integrated Security=True"
},

Appsettings.json dosyasındayken JWT için gerekli olan anahtar ve geçerlilik süresini de burada tutabileceğimizden geri dönmemek adına şimdi yazabiliriz.

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DevConnection": "Data Source=localhost;Initial Catalog=SecureDb;Integrated Security=True"
},
"JWT": {
"key": "C1CF4B7DC4C4175B6618DE4F55CA4",
"Issuer": "SecureApi",
"Audience": "SecureApiUser",
"DurationInMinutes": 60
}
}

appsettings.json dosyası altında işimiz bittikten sonra migration işlemine başlayalım.

Bir sonra ki sınıfımız ise bir kullanıcıya rol eklemesi yapılırken kullanacağımız sınıf. Bu sınıf içerisinde kullanıcıya ait olan mail, şifre ve rol bilgisini tutacak. Tools \ NuGet Package Manager \ Package Manager Console ' u açalım ve aşağıda ki 2 kodu sırasıyla yazalım. Oluşabilecek bir hata da Entity Framework varlığını ve versiyonunu kontrol etmenizi tavsiye ederim.

Add-Migration Initial
Update-Database

Artık gerekli tablolarımız veritabanımıza yansıtıldı. Şimdi sınıflarımızı oluşturmaya devam edelim.

Kullanıcının ilk olarak sistemde kayıtlı olduğunu bilmemiz var ise rollerini görmemiz gerekir. sonrasında ise authorization işlemi gerçekleştirilecektir.

Yeni bir sınıf oluşturalım ve adını AuthenticationModel olarak belirleyelim.

public struct AuthenticationModel
{
public string Message { get; set; }
public bool IsAuthenticated { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public List<string> Roles { get; set; }
public string Token { get; set; }
}
public class AddRoleModel
{
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string Role { get; set; }
}

Şimdi sıra geldi bir JWT sınıfı oluşturmaya. JWT ile ilgili bize gerekli olan verileri tutacak bir sınıf gerekmekte. Yeni bir sınıf oluşturup adını JWT olarak belirleyelim.

public class JWT
{
public string Key { get; set; }
public string Issuer { get; set; }
public string Audience { get; set; }
public double DurationInMinutes { get; set; }
}

Anahtar ve bu biletin geçerlilik süresi vs. üzerinde işlem yapmamız için gerekli alanları yazıyoruz.

Eklememiz gereken bir başka sınıf ise RegisterModel. Bu sınıfı ile bir kullanıcının sisteme kayıt olması aşamasında kullanacağız.

public class RegisterModel
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
[Required]
public string UserName { get; set; }
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}

Alanların gerekli olduğunu belirtmek için Required ile işaretlediğimizden emin olalım.

Yukarıda bahsettiğim gibi kullanıcının bilgilerinin doğruluğunu kontrol ettikten sonra kullanıcıya token vermemiz gerektiği. Bu sebeple kullanıcının mail ve şifresi üzerinde işlem yapabileceğimiz bir modele ihtiyacımız var. Yeni bir sınıf oluşturalım ve adını TokenRequestModel olarak belirleyelim.

public class TokenRequestModel
{
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}

Kullanıcı bu bilgileri kullanarak token isteğinde bulunabilecek. Artık sıra geldi servisimizi yazmaya. Bir interface üretelim ve kullanacağımız metot imzalarını yazalım.

public interface IUserService
{
Task<string> RegisterAsync(RegisterModel model);
Task<AuthenticationModel> GetTokenAsync(TokenRequestModel model);

Task<string> AddRoleAsync(AddRoleModel model);
}
  1. Kullanıcının sisteme üye olması için gereken ve kullanıcıdan bir RegisterModel bekleyen metot.
  2. Kullanıcının bilgilerini doğruladıktan sonra ona bir AuthenticationModel içerisinde Token döneceğimiz ve kendisinden TokenRequestModel beklediğimiz metot.
  3. Kullanıcıya rol bazlı işlem yapmak için AddRoleModel sınıfını beklediğimiz metot.

Şimdi bu oluşturduğumuz arayüzü yeni bir sınıf oluşturarak implement edelim.

public class UserService : IUserService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly JWT _jwt;
public UserService(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<JWT> jwt)
{
_userManager = userManager;
_roleManager = roleManager;
_jwt = jwt.Value;
}

Kullanacağımız metotları doldurmadan önce gerekli DI işlemlerini yapıyoruz. UserManager ve RoleManager sınıfları ile gelen kullanıcı bilgilerini doğrulayacağız. IOption ile JWT modeline uygun olarak appsettings.json içerisinde oluşturduğumuz alanlara ulaşabiliriz.

İlk fonksiyonumuz Register. Bu fonksiyon ile kullanıcı sisteme kayıt olabilecek

public async Task<string> RegisterAsync(RegisterModel model)
{
//ApplicationUser sınıfının instancesini alarak içerisini gelen modeldeki bilgilere göre doldurduk. Bu işlemi yapmamızın sebebi userManager sınıfı bizden parametre olarak bu modeli beklemektedir.
var user = new ApplicationUser
{
UserName = model.UserName,
Email = model.Email,
FirstName = model.FirstName,
LastName = model.LastName,

};

var userWithSameEmail = await _userManager.FindByEmailAsync(model.Email);
if (userWithSameEmail == null)
{
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(user, Authorization.default_role.ToString());
}
return $"User Registered {user.UserName}";
}
else
{
return $"Email {user.Email } is already registered.";
}


}
  1. ApplicationUser sınıfının instancesini alarak içerisini gelen modeldeki bilgilere göre doldurduk. Bu işlemi yapmamızın sebebi userManager sınıfı bizden parametre olarak bu modeli beklemektedir.
  2. Kullanıcının mail adresi ile varlığının kontrolünü sağlıyoruz.

3.Eğer kullanıcı yok ise artık kayıt işlemine başlayabiliriz. Bu durumda ilk olarak kullanıcı bilgisini FindByEmailAsync metodu ile kontrol ediyoruz. CreateAsync metoduna göndererek kullanıcıyı oluşturuyoruz. Oluşturulan kullanıcıya bir rol ataması için AddToRoleAsync metoduna default rol bilgisini veriyoruz.

“AddRoleAsync” metodumuz henüz hazır değil. Kullanıcıya rol ekleme işlemleri için aşağıda ki gibi metodu dolduralım.

public async Task<string> AddRoleAsync(AddRoleModel model)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user==null)
{
return $"No Accounts Registered with {model.Email}.";
}
if(await _userManager.CheckPasswordAsync(user,model.Password))
{
var roleExists = Enum.GetNames(typeof(Authorization.Roles))
.Any(x => x.ToLower() == model.Role.ToLower());
if (roleExists)
{
var validRole = Enum.GetValues(typeof(Authorization.Roles)).Cast<Authorization.Roles>()
.Where(x => x.ToString().ToLower() == model.Role.ToLower())
.FirstOrDefault();

await _userManager.AddToRoleAsync(user, validRole.ToString());
return $"Added {model.Role} to user {model.Email}.";
}
return $"Role {model.Role} not found.";
}
return $"Incorrect Credentials for user {user.Email}.";
}

Register işlemlerinde yapılanların bir benzerini burada yaparak ilk kullanıcıyı bulup sonrasında rol eklemesi yapıyoruz.

Sıra en önemli aşama olan kullanıcıya token üretmede.

public async Task<AuthenticationModel> GetTokenAsync(TokenRequestModel model)
{
AuthenticationModel authenticationModel;
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
return new AuthenticationModel { IsAuthenticated = false, Message = $"No Accounts Registered with { model.Email }." };

if (await _userManager.CheckPasswordAsync(user, model.Password))
{
JwtSecurityToken jwtSecurityToken = await CreateJwtToken(user);

authenticationModel = new AuthenticationModel
{
IsAuthenticated = true,
Message = jwtSecurityToken.ToString(),
UserName = user.UserName,
Token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken),
Email = user.Email,
};
var rolesList = await _userManager.GetRolesAsync(user).ConfigureAwait(false);
authenticationModel.Roles = rolesList.ToList();
return authenticationModel;
}
else
{
authenticationModel = new AuthenticationModel()
{
IsAuthenticated = false,
Message = $"Incorrect Credentials for user {user.Email}."
};
}


return authenticationModel;
}
  1. İlk olarak gelen modelde ki email bilgisine bakarak kullanıcının verilerini veritabanından çekiyoruz. Sonrasında şifre kontrolü yapıyoruz. Eğer bilgiler doğru ise yeni bir JwtSecurityToken instance alıp bunu CreateJwtToken metodundan dönecek olan token modeli ile dolduruyoruz.

Oluşturduğumuz AuthenticationModel içerisine kullanıcı ile ilgili token ve diğer bilgileri yazıyoruz.

Burada kullandığımız CreateJwtToken metodunu dolduralım.

private async Task<JwtSecurityToken> CreateJwtToken(ApplicationUser user)
{
var userClaims = await _userManager.GetClaimsAsync(user);
var roles = await _userManager.GetRolesAsync(user);
var roleClaims = new List<Claim>();
for (int i = 0; i < roles.Count; i++)
{
roleClaims.Add(new Claim("roles", roles[i]));
}
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim("uid", user.Id)
}
.Union(userClaims)
.Union(roleClaims);
var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.Key));
var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256);
var jwtSecurityToken = new JwtSecurityToken(
issuer: _jwt.Issuer,
audience: _jwt.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_jwt.DurationInMinutes),
signingCredentials: signingCredentials);
return jwtSecurityToken;
}

Rollerde dahil olmak üzere kullanıcı ile ilgili bilgilerini alıyoruz. Bir kullanıcının birden fazla rolü olabileceğinden döngü ile kontrol ediyoruz. Sonrasında kullanıcının username, email ve benzersiz bir anahtar oluşturup Claim dizisine ekliyoruz. Son bölümdeappsettings içerisinden key bilgisini okuyarak SymmetricSecurityKey oluşturuyoruz. Hemen altında credantial için oluşturduğumuz anahtar ve kullanacağımız algoritmayı yazıyoruz. Token oluşturmaya hazır hale geldiğinde yeni bir JwtSecurityToken modeli oluşturup bunu geri dönüyoruz.

Bir sonra ki aşamada Controller oluşturup yaptıklarımızı test etmek kaldı. İlk olarak sadece Authorize olan kullanıcılara bir değer döndürdüğümüz controller ve action oluşturalım. Controllerimiz route ayarlarını unutmayalım.

[Route("api/[controller]")]
[ApiController]
public class SecuredController : Controller
{
[HttpGet]
public IActionResult GetSecuredData()
{
return Ok("This Secured Data is available only for Authenticated Users.");
}
}

Bir sonra ki adımda servisimi call edeceğimiz Controlleri oluşturuyoruz. Controllerimiz route ayarlarını unutmayalım.

[Route("api/[controller]")]
[ApiController]

public class UserController : ControllerBase
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}


[HttpPost("register")]
public async Task<ActionResult> ResultAsync(RegisterModel register)
{
var result = await _userService.RegisterAsync(register);
return Ok(result);
}

[HttpPost("token")]
public async Task<IActionResult> GetTokenAsync(TokenRequestModel model)
{
var result = await _userService.GetTokenAsync(model);
return Ok(result);
}

[HttpPost("addrole")]
public async Task<IActionResult> AddRoleAsync(AddRoleModel model)
{
var result = await _userService.AddRoleAsync(model);
return Ok(result);
}
}

Tabi ki unutmadan startup.cs içerisinde gerekli konfigurasyonları yapmak zorundayız.

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

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
//AppSettings konfigurasyonu
services.Configure<JWT>(Configuration.GetSection("JWT"));

//DI User Service
services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();
services.AddScoped<IUserService, UserService>();

//DB Contextin eklenmesi
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DevConnection"),
b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));

// JWT
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.RequireHttpsMetadata = false;
o.SaveToken = false;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
ValidIssuer = Configuration["JWT:Issuer"],
ValidAudience = Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"]))
};
});
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "sametuca_dev_jwt_v1", Version = "v1" });
});
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "sametuca_dev_jwt_v1"));
}

app.UseHttpsRedirection();

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

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

Startup ayarlarımızda bittiğine göre artık uygulamamızı Postman üzerinden test edebiliriz.

Ben bu tip işlemlerde genel olarak Postman kullanmaktayım. Sizler farklı bir araç kullanabilirsiniz.

Postman Resmi Web Sitesi

Uygulamayı başlatın ve Postmanüzerinden aşağıda ki görseldeki seçeneklere göre post isteği atın.

User Registered hashcodemakale şeklinde mesajı gördük. Artık bu isimde bir kullanıcımız sistemde. Şimdi Token ile zenginleştirelim.

Artık kullanıcımıza ait sistem tarafından üretilmiş bir Token bulunmakta. Peki bu Tokeni nasıl kullanacağız? Hatırlarsanız sadece Authorize olmuş kullanıcıları kabul ettiğimiz bir Action vardı. Şimdi ona Token bilgimiz ile bir GET isteğinde bulunalım.

İşlem bu kadar. Umarım yardımcı olabilmişimdir. Zaman ayırdığınız için teşekkürler. Bir sonraki makalede görüşmek üzere sağlıcakla kalın..

--

--

Samet UCA
Samet UCA

No responses yet