ASP.NET Core Identity: Beginner to Advanced
ASP.NET Core Identity kullanıcılar üzerinde Authentication ve Authorization işlemlerini yürüten bir üyelik sistemidir. Identity API ile kullanıcı ve roller oluşturabiliriz; oluşturmuş olduğumuz kullanıcıların giriş, çıkış işlemlerini yönetebiliriz; rol, claim ilişkilerini yönetebiliriz; external login provider kullanarak Facebook, Twitter, Google gibi OAuth 2.0 protokolünü destekleyen uygulamalarla kullanıcıları sistemize login edebiliriz.
ASP.NET Core Identity‘i incelemeden Authentication ve Authorization kavramlarını inceleyip arasındaki farklara göz atalım.
Authentication kullanıcının kimlik bilgilerini (user credential) doğrulama işlemidir. Authorization ise kimliği doğrulanan kullanıcının sistem içerisinde sahip olduğu izinlerle hangi kaynaklara erişebileceğinin kontrol edildiği işlemdir.
Identity API‘yı iki bölümde inceleyeceğiz. Öncelikle Cookie-Based Authentication ile web uygulamalarının nasıl yetkilendirileceğine bakacağız. Sonrasındaysa Token-Based Authentication ile JWT (JSON Web Token) kullanarak API’ların nasıl koruma altına alınacağına bakacağız. Bu iki yazı dizisinde Role-Based Authorization, Policy-Based Authorization ve Claim-Based Authorization konuları da incelenecektir. Yazı serisinde ortak kullanılan genel özellikler tekrarı önlemek için burada tanıtılacaktır.
Identity API Kurulumu
Öncelikle ASP.NET Core Identity‘i kullanmak istediğimiz projeye altyapıyı sağlayacak gerekli Microsoft.AspNetCore.Identity.EntityFrameworkCore ve Microsoft.EntityFrameworkCore.SqlServer NuGet paketlerini kuralım.
ASP.NET Core Identity hali hazırda kullanıma hazır bir takım sınıflarla birlikte gelmektedir ve bu sınıflar veritabanı tablolarımızın bir yansımasıdır. Bu sınıflar akış ve gereksinimler doğrultusunda özelleştirmeye uygun esnek bir yapıdadır. Biz gereksinimlerimiz doğrultusunda kullanıcı ve rol sınıflarına müdahale ederek yeni özellikler katmak istiyoruz.
public enum Gender { Unknown, Male, Female }
public class AppUser : IdentityUser
{
public Gender Gender { get; set; }
public DateTime CreatedOn { get; set; }
}
Kullanıcı bilgilerini tutacak sınıfımızı IdentityUser<TKey> generic sınıfı üzerinden inherit ediyoruz. TKey ile Id property’sinin hangi türde olacağını belirtiyoruz, default haliyle string tipindedir.
public class AppRole : IdentityRole
{
public DateTime CreatedOn { get; set; }
}
Rol bilgilerini tutacak sınıf ise IdentityRole<TKey> generic sınıfı üzerinden inherit ediliyor. Id property tipini yine TKey ile belirtebiliyoruz.
Sonrasında DbContext sınıfımızı IdentityDbContext<TUser, TRole, TKey> generic sınıfından inherit alacak şekilde oluşturalım.
public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
}
Oluşturduğumuz DbContext ve Identity API ile ilgili servis ayarlarını Program.cs içerisinden ayarlayalım.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer"));
});
builder.Services.AddIdentity<AppUser, AppRole>().AddEntityFrameworkStores<AppDbContext>();
var app = builder.Build();
app.UseAuthentication();
app.Run();
DbContext‘imizi servis olarak ekledikten sonra AddIdentity<TUser, TRole> generic methoduyla varsayılan Identity sistem ayarlarını belirtilmiş kullanıcı ve rol tiplerinde ekliyoruz. Kullanıcı ve rol sınıflarını özelleştirmemiş olsaydık aşağıdaki gibi kullanmamız gerekecekti.
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>();
Takip eden method-chain’de AddEntityFrameworkStores generic methoduyla Identity bilgilerinin depolanacağı ilgili Entity Framework Core implementasyonunu ekliyoruz.
Identity Tabloları
Entity Framework Core kullanacağımızı bildirip bir migration oluşturarak veritabanı ve tablolarına göz atalım.
- AspNetUsers: Kullanıcıların tutulduğu tablodur, kolonlarına göz atalım.
- Id: Her bir kullanıcıya denk düşen primary key kolonudur.
- UserName: Kullanıcı adının tutulduğu kolondur.
- NormalizedUserName: UserName kolonunda sorgu performası için oluşturulan index kolonudur.
- Email: Kullanıcı e-posta adresinin tutulduğu kolondur.
- NormalizedEmail: Email kolonunda sorgu performası için oluşturulan index kolonudur.
- EmailConfirmed: E-posta adresinin doğrulanma durumunu tutan kolondur.
- PasswordHash: Kullanıcı şifresinin salt ve hashlenerek tutulduğu kolondur.
- ConcurrencyStamp: Concurrency sorunlarını aşmak için kullanılan kolondur. İki kullanıcının aynı kaydı güncellemek üzere açtığını düşünelim. İlk kullanıcı kaydı güncelleyip, ikinci kullanıcı da üstüne güncelleme işlemi yaptığında bu kolon değişmiş olacağından hata fırlatılacaktır.
- SecurityStamp: Kullanıcı kimlik bilgilerinin anlık görüntüsünü (snapshot) temsil eder. Kullanıcı şifresi değiştirildiğinde, External Login Provider ile yapılan girişler kaldırıldığında bu alan da değişerek eski cookie bilgileri geçersiz kılınacaktır.
- PhoneNumber: Kullanıcı telefon numarasının tutulduğu kolondur.
- PhoneNumberConfirmed: Telefon numarasının doğrulanma durumunu tutan kolondur.
- TwoFactorEnabled: İki aşamalı doğrulamanın aktifliğinin tutulduğu kolondur.
- LockoutEnabled: Başarısız giriş denemeleri sonucu hesabın kilitlenme durumunun tutulduğu kolondur.
- LockoutEnd: Hesabın hangi zamana kadar kilitli kalacağının tutulduğu kolondur.
- AccessFailedCount: Başarısız giriş denemelerinin tutulduğu kolondur.
- AspNetRoles: Rollerin tutulduğu tablodur, kolonlarına göz atalım.
- AspNetUserRoles: Kullanıcılara atanan rollerin tutulduğu ara tablodur.
- AspNetUserClaims: Kullanıcılara ilişkin key-value mantığında ekstra bilgilerin tutulduğu tablodur.
- AspNetRoleClaims: Rollere ilişkin key-value mantığında ekstra bilgilerin tutulduğu tablodur.
- AspNetUserLogins: External Login Provider kullanılarak yapılan girişlerle sistemdeki kullanıcıların eşleştirildiği tablodur.
- AspNetUserTokens: External Login Provider ile yapılan girişlerdeki authentication token’larının tutulduğu tablodur.
Manager Sınıfları
Identity API bizlere işlevlerine göre ayrışmış bir takım görevleri yerine getiren bazı Manager sınıfları da sunmaktadır.
- UserManager<TUser> kullanılarak kullanıcı oluşturma, güncelleme ve silme, kullanıcıyı belirli bir role ekleme ve silme, kullanıcıya bir claim ekleme ve silme, şifre değiştirme, e-posta doğrulama gibi kullanıcı işlemleri yerine getirilir. IdentityOptions tipindeki Options property’siyle genel ayarlara ulaşabiliriz.
- RoleManager<TRole> kullanılarak rol oluşturma, güncelleme ve silme, bir role claim ekleme ve silme gibi rol işlemleri yerine getirilir.
- SignInManager<TUser> kullanılarak kullanıcı giriş, çıkış ve iki aşamalı yetkilendirme gibi işlemleri yerine getirir. Options property’siyle genel ayarlara ulaşabiliriz.
Manager sınıflarımıza ihtiyaç duydukça DI üzerinden elde edeceğiz.
private readonly UserManager<AppUser> _userManager;
private readonly SignInManager<AppUser> _signInManager;
private readonly RoleManager<AppRole> _roleManager;
public Controller(UserManager<AppUser> userManager, SignInManager<AppUser> signInManager, RoleManager<AppRole> roleManager)
{
_userManager = userManager;
_signInManager = signInManager;
_roleManager = roleManager;
}
Ön bilgileri incelediğimize göre artık bu bilgiler doğrultusunda Identity API işlevselliğine odaklanabiliriz. Bir kullanıcı oluşturalım ve kullanıcıları listeleyelim.
var user = new AppUser
{
UserName = "burakneis",
Email = "[email protected]",
Gender = Gender.Male,
CreatedOn = DateTime.UtcNow
};
var result = await _userManager.CreateAsync(user, "1234");
if (result.Succeeded)
{
var confirmationToken = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var confirmationLink = Url.Action("ConfirmEmail", "Home", new
{
userId = user.Id,
token = confirmationToken
}, HttpContext.Request.Scheme);
await _emailHelper.SendAsync(new()
{
Subject = "Confirm e-mail",
Body = $"Please <a href='{confirmationLink}'>click</a> to confirm your e-mail address.",
To = user.Email
});
}
Yukarıda göreceğiniz üzere UserManager sınıfının CreateAsync methoduyla örneklenen AppUser sınıfıyla bir kullanıcı oluşturuluyor. Sonrasındaysa GenerateEmailConfirmationTokenAsync methoduyla kullanıcıya e-posta adresini doğrulaması için bir token üretilerek e-posta gönderiliyor.
Kullanıcı şifre sıfırlaması, e-posta ve telefon doğrulaması, 2FA yetkilendirme gibi işlemlerde token üretmek için AddIdentity ile eklediğimiz yetkilendirme mekanizmasına AddDefaultTokenProviders methoduyla varsayılan token provider eklenmelidir. Üretlien token’lar memory üzerinde saklanmaktadır.
builder.Services.AddIdentity<AppUser, AppRole>(options =>
{
// options
}).AddDefaultTokenProviders();
Kayıt olan kullanıcıya gönderdiğimiz e-postaya tıklanmasıyla e-posta adresinin doğrulanacağı kod bloğunu yazalım.
var user = await _userManager.FindByIdAsync(userId);
if (user != null)
{
var result = await _userManager.ConfirmEmailAsync(user, token);
if (result.Succeeded)
{
return RedirectToAction("Login");
}
}
Email alanı doğrulanması gereken kullanıcıyı FindByIdAsync methoduyla elde ediyoruz. Kullanıcı kayıt olurken oluşturulan token kullanılarak ConfirmEmailAsync methoduyla kullanıcı e-posta adresini doğruluyoruz.
Kullanıcı listelesini elde etmek için UserManager sınıfının Users property’sini kullanıyoruz.
var users = _userManager.Users;
Görüldüğü üzere kullanıcılarımızı IQueryable<TUser> tipinde elde ettik.
Identity API Validations
Oluşturduğumuz yapıya dikkat ederseniz kullanıcı adı Türkçe karakter içeremiyor ve herhangi bir şifre kriteri mevcut olmadığından tek karakterle dahi şifre belirleyebiliyoruz.
User Validations
Kullanıcıyla ilgili ayarları akışımız doğrultusunda düzenleyelim. UserName alanı varsayılan olarak unique ve bu özellik değiştirilemez. Sadece izin verilen karakterlere müdahale edebiliriz. Email alanının da unique olup olmayacağını belirtebiliriz.
// abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+
builder.Services.AddIdentity<AppUser, AppRole>(options =>
{
options.User.AllowedUserNameCharacters = "abcçdefgğhiıjklmnoöpqrsştuüvwxyzABCÇDEFGĞHIİJKLMNOÖPQRSŞTUÜVWXYZ0123456789-";
options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<AppDbContext>();
Yukarıda varsayılan olarak izin verilen karakterler yorum satırında belirtilmiştir. UserOptions üzerinden AllowedUserNameCharacters property’siyle Türkçe karakterleri eklemiş bulunmaktayız. Sonraki satırda RequireUniqueEmail ile girilen e-posta adreslerinin unique (tekil) olması gerektiğini belirlemiş oluyoruz.
Ayrıca User üzerinde başka bir takım validasyonlar sağlamak istiyorsak IUserValidator<TUser> arayüzünü implemente etmeliyiz. Kullanıcı adının en az 6 karakterden oluşmasını ve e-posta adıyla kullanıcı adının aynı olmamasını istiyoruz.
public class UserValidator : IUserValidator<AppUser>
{
public Task<IdentityResult> ValidateAsync(UserManager<AppUser> manager, AppUser user)
{
var errors = new List<IdentityError>();
if (user.UserName.Length < 6)
{
errors.Add(new() { Code = "UserNameLength", Description = "User name must be at least 6 characters." });
}
if (user.Email.Substring(0, user.Email.IndexOf("@")) == user.UserName)
{
errors.Add(new() { Code = "UserNameContainsEmail", Description = "User name can not contains email name." });
}
if (errors.Any())
{
return Task.FromResult(IdentityResult.Failed(errors.ToArray()));
}
return Task.FromResult(IdentityResult.Success);
}
}
UserValidator sınıfımızı kullanacağımızı AddIdentity ile eklediğimiz Identity yapısına AddUserValidator methoduyla bildirelim.
builder.Services.AddIdentity<AppUser, AppRole>(options =>
{
// options
}).AddUserValidator<UserValidator>();
Password Validations
Şifre için de bir takım ayarları yine aynı şekilde IdentityOptions üzerinden yapabilir ve ilave validasyonları IPasswordValidator<TUser> arayüzünü implemente ederek sağlayabiliriz.
builder.Services.AddIdentity<AppUser, AppRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequiredUniqueChars = 1;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = true;
});
PasswordOptions özelliği üzerinden yapmış olduğumuz ayarlar aşağıdaki gibidir.
- RequireDigit ile şifrenin rakam içermesi gerektiğini,
- RequiredLength ile şifrenin en az 8 karakter olması gerektiğini,
- RequiredUniqueChars ile şifrenin tekrarlanan karakter sayısının 1 olması gerektiğini,
- RequireLowercase ile şifrenin küçük harf içermesi gerektiğini,
- RequireUppercase ile şifrenin büyük harf içermesi gerektiğini,
- RequireNonAlphanumeric ile şifrenin alfanümerik olmayan karakterler içerebileceğini belirlemiş olduk.
Şimdiyse IPasswordValidator interface‘ini implemente ederek şifrenin kullanıcı adını içermemesi gerektiğini belirtelim.
public class PasswordValidator : IPasswordValidator<AppUser>
{
public Task<IdentityResult> ValidateAsync(UserManager<AppUser> manager, AppUser user, string password)
{
var errors = new List<IdentityError>();
if (user.UserName == password)
{
errors.Add(new() { Code = "PasswordContainsUsername", Description = "Password can not contains user name." });
}
if (errors.Any())
{
return Task.FromResult(IdentityResult.Failed(errors.ToArray()));
}
return Task.FromResult(IdentityResult.Success);
}
}
Artık PasswordValidator sınıfımızı Identity mekanizmasına AddPasswordValidator methoduyla dahil edebiliriz.
builder.Services.AddIdentity<AppUser, AppRole>(options =>
{
// options
}).AddPasswordValidator<PasswordValidator>();
Hata Mesajlarının Özelleştirilmesi
Şimdiye kadar yaptığımız ayarlamalar ve validasyon gereksinimleri doğrultusunda geçersiz bir veri girilmesi durumunda hata alındığını farkedeceksiniz. Hatalar varsayılan olarak İngilizce dilindedir. Çoklu dil desteği olan bir yapıda mevcut mesajların ezilerek farklı dillere çevrilmesi gerekmektedir. Identity API ile bu işlemi IdentityErrorDescriber sınıfıyla yapmaktayız.
public class ErrorDescriber : IdentityErrorDescriber
{
public override IdentityError InvalidUserName(string userName) => new() { Code = "InvalidUserName", Description = $"\"{userName}\" kullanıcı adı geçersiz." };
public override IdentityError DuplicateEmail(string email) => new() { Code = "DuplicateEmail", Description = $"\"{email}\" adresi kullanımdadır." };
public override IdentityError PasswordTooShort(int length) => new() { Code = "PasswordTooShort", Description = $"Şifre en az {length} karakter olmalıdır." };
}
Validator arayüzleriyle implemente ettiğimiz sınıflardaki hata mesajlarını da bu sınıfa toplayarak daha temiz bir kod elde ederek yönetimi merkezileştirebiliriz. Sonrasındaysa AddIdentity ile sistemimize dahil ettiğimiz yetkilendirme mekanizmasına bu mesajları kullanması gerektiğini bildiriyoruz.
builder.Services.AddIdentity<AppUser, AppRole>(options =>
{
// options
}).AddErrorDescriber<ErrorDescriber>();
Kullanıcı Yönetimi
Buraya kadar sistemimizdeki kullanıcıları listeledik, yeni kullanıcı ekledik ve eklenirken nelere dikkat edilmesi gerektiğini belirledik. Yazının kalan kısmında kullanıcıların giriş, çıkış yapmalarını; şifre işlemlerini; bir role ve claim’e sahip olmalarını; rol ve claim’ler doğrultusunda yetkilendirme işlemlerini gerçekleştireceğiz.
Oturum İşlemleri
Konuya giriş yapmadan kullanıcıların başarısız giriş denemeleri sonunda kilitlenmesi konusunda bilgi sahibi olmamız gerekiyor. Kilitlenmeyle ilgili genel ayarları LockoutOptions üzerinden yapmaktayız.
builder.Services.AddIdentity<AppUser, AppRole>(options =>
{
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
});
Başarısız giriş sayısının 5 olduğunu MaxFailedAccessAttempts özelliği, her kilitlenme durumunda 15 dakika beklenmesi gerektiğini DefaultLockoutTimeSpan özelliğiyle belirledik.
UserManager sınıfı Lockout ile ilgili kullanımımıza bir takım methodlar sunmaktadır.
- IsLockedOutAsync ile kullanıcı hesabının kilitlenip kilitlenmediğine bakabiliyor,
- AccessFailedAsync ile kullanıcının giriş denemesinin başarısız olduğunu belirtiyor,
- GetAccessFailedCountAsync ile kaç kez başarısız giriş denemesi yapıldığını elde ediyor,
- ResetAccessFailedCountAsync ile AccessFailedCount alanı sıfıra çekiliyor,
- GetLockoutEndDateAsync ile hesabın kilitli kalacağı zamanı elde ediyor,
- SetLockoutEndDateAsync ile hesabın kilitli kalacağı zamanı ayarlıyoruz.
Verilen bilgiler ışığında UserManager ve SignInManager‘ı kullanarak giriş işlemlerini Lockout özelliği eşliğinde sağlayalım.
var user = await _userManager.FindByEmailAsync(viewModel.Email);
if (user != null)
{
await _signInManager.SignOutAsync();
var result = await _signInManager.PasswordSignInAsync(user, viewModel.Password, viewModel.RememberMe, true);
if (result.Succeeded)
{
await _userManager.ResetAccessFailedCountAsync(user);
await _userManager.SetLockoutEndDateAsync(user, null);
// Ok
}
else if (result.IsLockedOut)
{
var lockoutEndUtc = await _userManager.GetLockoutEndDateAsync(user);
var timeLeft = lockoutEndUtc.Value - DateTime.UtcNow;
// Account locked out, try again {timeLeft.Minutes} minutes later.
}
else
{
// "Invalid e-mail or password."
}
}
Öncelikle FindByEmailAsync methoduyla sistemde gönderilen e-posta adresine sahip olan kullanıcı varlığı kontrol ediliyor. Kullanıcı bulunması durumunda önceki oturumdan kalma olası cookie bilgilerini temizlemek için arkaplanda SignOutAsync methoduyla çıkış işlemi gerçekleştiriliyor. Oturum açmak üzere PasswordSignInAsync methodu, kullanıcının gönderdiği credential bilgileriyle çağırılıyor. Bu method üzerinde dikkatinizi çekmek istediğim iki nokta var. Method overload’ları incelendiğinde isPersistent ve lockoutOnFailure parametrelerini alan bir method mevcut. isPersistent parametresine true argümanı verilirse oturum cookie’de belirtilen süre boyunca aksi durumda session kadar olacaktır. lockoutOnFailure parametresine true argümanı verilirse lockout yönetimi Identity API’a devrediliyor. Aksi durumdaysa kendi lockout akışımızı yukarıda belirtilen methodlar doğrultusunda yazabiliriz. Giriş işleminin başarılı olmasıyla, giriş işlemi başarılı olana değin yapılan hatalı girişleri ResetAccessFailedCountAsync methoduyla sıfırlıyoruz. Hesap, önceki başarısız giriş denemeleriyle kilitlenmişse SetLockoutEndDateAsync methoduyla sıfırlanıyoruz.
await _signInManager.SignOutAsync();
Kullanıcımızı SignOutAsync methodunu kullanarak çıkış yaptırıyoruz.
ASP.NET Core Security Context
Bir kullanıcı sistemde oturum açtığında kullanıcı hakkındaki belirli bilgilere ControllerBase sınıfının ClaimsPrincipal tipindeki User property’si üzerinden erişebiliriz.
ClaimsPrincipal nesnesini kullanıcıyı temsil eden bir security context olarak tanımlayabiliriz. Kullanıcı hakkındaki UserName ve Email gibi bilgileri kapsüller. Bir Principal ClaimsIdentity tipinde bir ve birden çok Identity içerir, çoğu senaryoda bir adet Identity kullanıyor olacağız. Identity’lerden her birini bir sürücü belgesi, pasaport, facebook login, google login gibi düşünelim ve neden birden fazla Identity’e sahip olabileceğimizi örneklendirelim. Havalimanında sadece pasaportumuzu sunmamız iddia ettiğimiz kişi olduğumuz anlamına gelmez. Olduğumuz kişi dışında birisinin pasaportunu da sunabiliriz. İkinci bir kimlik doğrulama olarak yüzümüzü de göstermemiz gerekmektedir. İşte bu gibi durumlar birden fazla Identity eşliğinde kimlik doğrulamayı gerektirir.
Identity’ler ise key-value çiftlerinden oluşan ve kullanıcı hakkında bilgiler tutan Claim tipindeki claim’lerden oluşur. Konuyu pekiştirme amaçlı ufak bir örnek aşağıda paylaşılmıştır.
var licenceClaims = new List<Claim>
{
new(ClaimTypes.Name, "Burak"),
new("LicenceType", "B"),
new("ValidUntil", "2022-09"),
};
var passportClaims = new List<Claim>
{
new(ClaimTypes.Name, "Burak"),
new("ValidUntil", "2028-07"),
new(ClaimTypes.Country, "Turkey")
};
var licenceIdentity = new ClaimsIdentity(licenceClaims, "LicenceIdentity");
var passportIdentity = new ClaimsIdentity(passportClaims, "PassportIdentity");
var userPrincipal = new ClaimsPrincipal(new[] { licenceIdentity, passportIdentity });
var authenticationProperties = new AuthenticationProperties { IsPersistent = true };
await HttpContext.SignInAsync(userPrincipal, authenticationProperties);
Identity API buna benzer işlem yaparak giriş yapan kullanıcı bilgilerini veritabanından çekerek ilgili verileri doldurur ve HttpContext üzerinden arkaplanda SignInAsync methodunu çağırır. Bu yapı kullanılarak custom membership yapısı da kurabiliriz.
User property’si üzerinden aşağıdaki bilgilere ulaşabiliriz.
- Identity üzerinden Name ile kullanıcı adına, IsAuthenticated ile yetkilendirilme durumuna, AuthenticationType ile kullanılan yetkilendirme tipine ulaşabiliriz.
- Identities üzerinden Identity’lere ulaşabiliriz.
- Claims üzerinden Principal üzerinde tanımlı tüm Identity claim’lerine erişebiliriz.
Oturum açmış kullanıcı bilgilerinin güncelleneceği kod bloğuna göz atalım.
var me = await _userManager.FindByNameAsync(User.Identity?.Name);
if (me != null)
{
if (me.PhoneNumber != viewModel.PhoneNumber && _userManager.Users.Any(a => a.PhoneNumber == viewModel.PhoneNumber))
{
// Phone number already in use.
}
else
{
me.UserName = viewModel.UserName;
me.Email = viewModel.Email;
me.PhoneNumber = viewModel.PhoneNumber;
me.Gender = viewModel.Gender;
var result = await _userManager.UpdateAsync(me);
if (result.Succeeded)
{
await _userManager.UpdateSecurityStampAsync(me);
await _signInManager.SignOutAsync();
await _signInManager.SignInAsync(me, true);
// Ok
}
}
}
else
{
await _signInManager.SignOutAsync();
}
UserManager sınıfı aracılığıyla FindByNameAsync methoduyla öncelikle kullanıcı elde ediliyor. Kullanıcı adımızı elde ederken ClaimsPrincipal nesnesinden faydalanıyoruz. Telefon numaralarının unique olmasını istediğimiz için telefon numarasının kullanılıp kullanılmadığını kontrol ediyoruz. Her şey yolundaysa UpdateAsync methoduyla kullanıcımızı güncelliyoruz. İşlem başarılıysa kullanıcının SecurityStamp alanını UpdateSecurityStampAsync methoduyla güncelleyerek silent sign-in yaptırıp sisteme tekrar login ediyoruz. Aynı işlemleri şifre değiştirme özelliğini kazandırmak istediğimizde de yapmalıyız.
Şifre İşlemleri
Kullanıcı işlemleriyle ilgili şifre sıfırlama konusuna da değinerek rol işlemlerine giriş yapalım.
var user = await _userManager.FindByEmailAsync(viewModel.Email);
if (user != null)
{
var passwordResetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
var passwordLink = Url.Action("ResetPassword", "User", new
{
userId = user.Id,
token = passwordResetToken
}, HttpContext.Request.Scheme);
await _emailHelper.SendAsync(new()
{
Subject = "Reset password",
Body = $"Please <a href='{passwordLink}'>click</a> to reset your password.",
To = user.Email
});
// Ok
}
ViewModel üzerinden gelen e-posta adresine sahip kullanıcıyı FindByEmailAsync methoduyla buluyoruz. Akabinde GeneratePasswordResetTokenAsync methoduyla şifre sıfırlanması için bir token üretiliyor ve kullanıcıya e-posta gönderiliyor.
Kullanıcı aldığı şifre sıfırlama e-postasındaki bağlantıya tıkladığında çalışacak kod bloğunu yazalım.
var user = await _userManager.FindByIdAsync(viewModel.UserId);
if (user != null)
{
var result = await _userManager.ResetPasswordAsync(user, viewModel.Token, viewModel.Password);
if (result.Succeeded)
{
await _userManager.UpdateSecurityStampAsync(user);
// Ok
}
}
Öncelikle kullanıcıyı FindByIdAsync methoduyla buluyoruz. Sonrasındaysa ResetPasswordAsync methoduyla bir önceki kod bloğunda oluşturduğumuz token üzerinden yeni şifremizi belirliyoruz. Akabinde SecurityStamp alanını güncellemek üzere UpdateSecurityStampAsync methodunu kullanıyoruz.
Rol Yönetimi
Kullanıcılarla ilgili işlemler tamamladık. Artık sisteme yeni roller oluşturabiliriz, mevcut rolleri güncelleyebiliriz ve silebiliriz. Oluşturduğumuz rollere yeni kullanıcılar atayabiliriz. Rol ile ilgili işlemleri RoleManager sınıfı üzerinden gerçekleştiriyoruz.
var roles = _roleManager.Roles;
RoleManager sınıfının Roles property’si üzerinden mevcut rolleri IQueryable<TRole> tipinde elde edebiliyoruz.
var isUpdate = viewModel.Id != null;
var role = isUpdate ? await _roleManager.FindByIdAsync(viewModel.Id) : new AppRole() { Name = viewModel.Name, CreatedOn = DateTime.Now };
if (isUpdate)
{
role.Name = viewModel.Name;
}
var result = isUpdate ? await _roleManager.UpdateAsync(role) : await _roleManager.CreateAsync(role);
if (result.Succeeded)
{
// Ok
}
Upsert işleminin yapıldığı kapsamda FindByIdAsync methoduyla ilgili rol elde ediliyor. Güncelleme işlemleri için UpdateAsync, ekleme işlemleri için CreateAsync ve silme işlemleri için DeleteAsync methodlarını kullanıyoruz.
var role = await _roleManager.FindByIdAsync(id);
if (role != null)
{
var result = await _roleManager.DeleteAsync(role);
if (result.Succeeded)
{
// Ok
}
}
Artık kullanıcıları oluşturduğumuz rollerle ilişkilendirebiliriz.
var user = await _userManager.FindByIdAsync(viewModel.Id);
if (user != null)
{
foreach (var item in viewModel.Roles)
{
if (item.IsAssigned)
{
await _userManager.AddToRoleAsync(user, item.RoleName);
}
else
{
await _userManager.RemoveFromRoleAsync(user, item.RoleName);
}
}
}
Kullanıcı FindByIdAsync methoduyla elde ediliyor. Kullanıcıya rol eklemek için AddToRoleAsync methodu kullanılıyor. Kullanıcı rolünün silinmesi için RemoveFromRoleAsync methodu kullanılıyor.
var user = await _userManager.FindByIdAsync(id);
var userRoles = await _userManager.GetRolesAsync(user);
Kullanıcı rolleri için öncelikle FindByIdAsync methoduyla kullanıcı elde ediliyor. Bu kullanıcı üzerinden GetRolesAsync methoduyla kullanıcı rollerini IList<string> tipinde elde ediyoruz.
Identity API Authorization
Kullanıcı ve rollerle ilgili işlemlerimizi sağladık. Artık kullanıcı kimliğini doğruladığında nereye, nasıl erişim sağlayabileceklerine odaklanabiliriz. Kimlik yetkilendirmeyi (Authorization) aktif etmek için Program.cs içerisinden UseAuthorization methodunu middleware olarak eklemeliyiz.
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.Run();
UseAuthorization methodu UseAuthentication methodu altına eklenmelidir. Yetkilendirme işlemleri için [Authorize] attribute’ünü kullanacağız. Authorize attribute’ünü Controller sınıfı Action methoduna vererek ilgili Action methodunun yetkilendirmeye tabi olacağını belirtebiliriz.
public class AdminController : Controller
{
[Authorize]
public IActionResult Index() => View();
}
Action methodu yerine doğrudan Controller sınıfını da işaretleyebiliriz.
[Authorize]
public class AdminController : Controller
{
public IActionResult Index() => View();
public IActionResult Roles() => View();
public IActionResult Users() => View();
[AllowAnonymous]
public IActionResult Error() => View();
}
Controller sınıfını Authorize ile işaretlendiğinden dolayı tüm Action methodlar yetkilendirmeye tabi olacaktır. Error Action methodunun yetkilendirmeye tabi olmamasını istiyoruz. Bu gibi durumlarda [AllowAnonymous] attribute’ünü kullanmamız gerekmektedir.
Özel gereksinimler doğrultusunda custom authorize attribute de yazabiliriz, çoğu durumda kendi yapımızı kullanıyor olacağız.
Role-Based Authorization
Oluşturduğumuz kullanıcılara roller atayıp Authorize attribute’ünün Roles property’sini kullanarak sayfalarımızı role-based authorization‘a tabi tutabiliriz. Roles property’sine birden çok rol verebiliriz. Verilen değerler case-sensitive olduğundan eşleşme sağlanmalıdır.
public class SiteController : Controller
{
[Authorize(Roles = "Admin")]
public IActionResult Dashboard() => View();
[Authorize(Roles = "Moderator, Admin")]
public IActionResult Users() => View();
[Authorize(Roles = "Editor, Moderator, Admin")]
public IActionResult Articles() => View();
[Authorize(Roles = "User, Editor, Moderator, Admin")]
public IActionResult Profile() => View();
}
Controller sınıfı incelendiğinde Dashboard Action methoduna Admin rolüne sahip kullanıcılar, Users Action methodunaysa Moderator ve Admin rolüne sahip kullanıcılar erişebilmektedir. Takip eden Action methodlarında da aynı mantık işlemektedir.
Claim-Based Authorization
Claim’ler kullanıcı hakkında key-value pair şeklinde (name: burak, email: [email protected] gibi) bilgi tutan yapılardır. Role-Based Authorization yapısına esneklik kazandırarak rol haricinde claim’lerle de yetkilendirmeye imkan sunar.
Şiddet içeren bir sayfamız olduğunu düşünelim. Kullanıcının sayfayı görüntüleyebilmesi için 18 yaş üzerinde olması gerekmektedir. Bu sorunu rol bazlı yetkilendirmeyle çözemeyiz. Kullanıcı yaşını doğrulamak için doğum tarihini claim olarak ekleyerek claim-based authorization yapılmalıdır. Yine ücrete tabi bir servisimiz olduğunu ve ilk 30 gün boyunca free-trial özelliğimiz olduğunu düşünelim. Kullanıcı kayıt tarihini yine claim olarak ekleyerek claim bazlı yetkilendirme yapmamız gerekmektedir.
Uygulamamız web uygulamasıysa (mvc gibi) claim’ler cookie üzerinde tutulmakta ve ilgili veriye buradan erişilmektedir. Web Api kullanıyor ve iletişimi jwt kullanarak haberleştiriyorsak claim’ler ilgili token payload‘ında tutularak taşınmaktadır.
Kullanıcıyı claim-based yetkilendirmeye tabi tutmak için ilgili claim’in eklenmesi gerekmektedir. Kullanıcı düzenleme sayfasında kullanıcıya departmanıyla ilgili bir claim ekleyelim.
var userClaims = await _userManager.GetClaimsAsync(user);
if (!userClaims.Any(a => a.Type == "Department"))
{
await _userManager.AddClaimAsync(user, new Claim("Department", "HR"));
}
Kullancının sahip olduğu claim’ler GetClaimsAsync methoduyla elde ediliyor. Kullanıcıda bu claim yoksa AddClaimAsync methoduyla oluşturuyoruz.
ClaimsPrincipal sınıfına ilave claim’leri IClaimsTransformation arayüzünü implemente ederek ekleyebiliriz.
public class ClaimsTransformation : IClaimsTransformation
{
private readonly UserManager<AppUser> _userManager;
public ClaimsTransformation(UserManager<AppUser> userManager)
{
_userManager = userManager;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = principal.Identity as ClaimsIdentity;
var user = await _userManager.FindByNameAsync(identity?.Name);
if (user != null)
{
if (!principal.HasClaim(c => c.Type == ClaimTypes.Gender))
{
var genderClaim = new Claim(ClaimTypes.Gender, Enum.GetName(user.Gender)!);
identity?.AddClaim(genderClaim);
}
if (!principal.HasClaim(c => c.Type == "FreeTrial"))
{
var freeTrialClaim = new Claim("FreeTrial", user.CreatedOn.ToShortDateString());
identity?.AddClaim(freeTrialClaim);
}
}
return principal;
}
}
Tanımlı ClaimTypes tiplerinden Gender sabiti tipinde yeni claim ClaimsPrincipal sınıfına ekleniliyor ve FreeTrial tipinde custom tipli bir claim ekleniliyor. ClaimsTransformation sınıfımızı DI container’e scoped olarak ekleyelim.
builder.Services.AddScoped<IClaimsTransformation, ClaimsTransformation>();
Claim’lerimizi doğrulayacak Policy’ler oluşturmalıyız. Bu işlem için AddAuthorization methodundan faydalanacağız.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("HrDepartmentPolicy", policy =>
{
policy.RequireClaim("Department", "HR");
});
options.AddPolicy("EmployeePolicy", policy =>
{
policy.RequireClaim("Department", "HR", "Accounting", "Finance");
});
});
Controller sınıfımızdaki Action methodlarımızı Authorize attribute ile işaretleyip Policy property’siyle oluşturduğumuz Policy’leri belirtelim.
[Authorize(Roles = "Employee")]
public class SiteController : Controller
{
[Authorize(Policy = "HrDepartmentPolicy")]
public IActionResult HR() => View();
[Authorize(Policy = "EmployeePolicy")]
public IActionResult Employee() => View();
}
Yukarıda rol ve claim bazlı yetkilendirme yapmış bulunmaktayız. Controller sınıfımıza Employee rolündeki kullanıcılar erişecek. Action methodlarınaysa yalnızca verilen Policy‘lere göre koşulu sağlayan kullanıcılar erişim sağlayacaktır.
Policy-Based Authorization
Policy oluşturduğumuzda bir claim’den fazlasına ihtiyaç duyabiliriz. Gereksinimler doğrultusunda bir takım koşulların sağlanması gerekebilir. Role-based Authorization ve Claim-Based Authorization yapıları da arkaplanda bir requirement handler ile preconfigured-policy kullanır. Bir requirement oluşturmak için IAuthorizationRequirement arayüzüyle AuthorizationHandler<TRequirement> abstract sınıflarından faydalanacağız.
- IAuthorizationRequirement ile authorization requirement’i temsil ederek yetkilendirme işleminin başarılı olup olmadığını belirler.
- AuthorizationHandler<TRequirement> requirement tarafından çağırılarak gereksinimler doğrultusunda şartların sağlanıp sağlanmadığını kontrol eden sınıftır.
ClaimsTransformation sınıfıyle eklemiş olduğumuz FreeTrial tipindeki claim ile servisimiz kullanıcıya 30 gün boyunca free-trial hizmet verecektir.
public class FreeTrialExpireRequirement : IAuthorizationRequirement { }
public class FreeTrialExpireHandler : AuthorizationHandler<FreeTrialExpireRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FreeTrialExpireRequirement requirement)
{
var freeTrialClaim = context.User.FindFirst(f => f.Type == "FreeTrial");
if (freeTrialClaim != null)
{
var freeTrialExpires = Convert.ToDateTime(freeTrialClaim.Value).AddDays(30);
if (DateTime.Now < freeTrialExpires)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
return Task.CompletedTask;
}
}
FreeTrial tipindeki claim elde edilerek kullanıcının servisten ücretsiz yararlanma hakkının geçerli olması durumu kontrol ediliyor. Kontrolün başarılı olması durumunda AuthorizationHandlerContext sınıfının Succeed methodu ve başarısız olması durumundaysa Fail methodu çağırılıyor.
Oluşturduğumuz Handler’i DI container’a, requirement’ı ise Policy olarak ekleyelim.
builder.Services.AddScoped<IAuthorizationHandler, FreeTrialExpireHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("FreeTrialPolicy", policy =>
{
policy.Requirements.Add(new FreeTrialExpireRequirement());
});
});
Controller sınıfımızın Action methodunu işaretleyelim.
public class SiteController : Controller
{
[Authorize(Policy = "FreeTrialPolicy")]
public IActionResult FreeTrial() => View();
}
Two Factor Authentication (2FA)
Günümüzde çoğu uygulama kullanıcılarını kullanıcı adı/e-posta ve şifre bilgilerini doğrulayarak sistemlerine login ediyor. Bu One Factor Authentication olarak adlandırılmaktadır. Two Factor Authentication ise kullanıcı doğrulama işlemini ikiye böler. Kullanıcı credential bilgilerini girdiğinde ikinci bir şifreyle kimlik doğrulama işlemi yapılmaktadır. İkinci doğrulama işlemiyse sms, e-posta, authenticator kullanılarak yapılmaktadır. Sms ile doğrulama işleminde kullanıcı cep telefonuna kısa bir kod alır. Benzer şekilde e-postayla doğrulama işleminde kullanıcı e-posta hesabına kısa bir kod alır.
Authenticator ise kullanıcılar mobil cihazlarına bir uygulama (Google/Microsoft Authenticator) kurar. Kullanıcıya bir karekod ile kurtarma şifreleri verilmektedir. Kullanıcı bu karekodu mobil cihazından taratarak eşleştirme yapar. Kullanıcıya özel Shared Key ve Timestamp değeri kullanılarak HMAC-Based One-Time Password algoritması kullanılarak her 30 saniyede yenilenen tek kullanımlık bir şifre elde ederiz. Belirli bir algoritma doğrultusunda oluşturulduğundan dolayı zaman dilimleri (Timestamp) aynı oldukça tüm Authenticator uygulamalarında aynı şifreler üretilecektir.
Sistemimizde birden çok 2FA doğrulama yöntemini destekleyeceğimiz için AppUser sınıfımıza ilgili kolonu ekleyip migration işlemleri yapalım.
public enum TwoFactorType { None, Sms, Email, Authenticator }
public class AppUser : IdentityUser
{
public TwoFactorType TwoFactorType { get; set; }
}
Öncelikle kullanıcının 2FA kullanarak giriş yapabilmesi için kullanıcı ayarlarından aktif etmelidir.
var user = await _userManager.FindByNameAsync(User.Identity?.Name);
user.TwoFactorType = viewModel.TwoFactorType;
await _userManager.UpdateAsync(user);
await _userManager.SetTwoFactorEnabledAsync(user, user.TwoFactorType != Models.TwoFactorType.None);
Kullanıcı FindByNameAsync methoduyla elde edilerek ilgili 2FA değişiklikleri yapılıyor. Doğrulama yöntemi olarak Authenticator seçildiğinde kullanıcıya mobil cihazlarıyla eşleştirebileceği bir Qr Code ve SharedKey vermeliyiz. Kullanıcı oluşturulan karekodu cihazına okutamadığı durumlarda verilen kodu girerek eşleştirme yapar.
var user = await _userManager.FindByNameAsync(User.Identity?.Name);
var authenticatorKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (authenticatorKey == null)
{
await _userManager.ResetAuthenticatorKeyAsync(user);
authenticatorKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
var authenticationUri = _twoFactorAuthService.GenerateQrCodeUri(user.Email, authenticatorKey);
Kullanıcı elde edilerek AspNetUserTokens tablosundan GetAuthenticatorKeyAsync methoduyla SharedKey çekilmeye çalışılıyor. Kayıt yoksa ResetAuthenticatorKeyAsync ile yeni bir kayıt oluşturulup tekrar çekiliyor. Qr Code kütüphanelerinin kullanacağı bir URI oluşturularak kullanıcıya dönülmektedir.
public string GenerateQrCodeUri(string email, string authenticatorKey)
{
var encodedUrl = _urlEncoder.Encode("www.localhost.com");
var encodedEmail = _urlEncoder.Encode(email);
return $"otpauth://totp/{encodedUrl}:{encodedEmail}?secret={authenticatorKey}&issuer={encodedUrl}&digits=6";
}
Methodu incelersek DI ile alınan UrlEncoder sınıfıyla kullanıcı email ve issuer alanları encode ediliyor. Qr Code kütüphaneleri oluşturulan bu URI‘yi kullanarak render edecekler.
Kullanıcı bu karekodu okutarak her 30 saniyede bir alacağı kodu sisteme girerek verify etmelidir.
var user = await _userManager.FindByNameAsync(User.Identity?.Name);
var isTokenValid = await _userManager.VerifyTwoFactorTokenAsync(user, _userManager.Options.Tokens.AuthenticatorTokenProvider, viewModel.VerificationCode);
if (isTokenValid)
{
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 5);
}
Kullanıcı elde edilerek gönderilen Authenticator kodu VerifyTwoFactorTokenAsync methoduyla verify ediliyor. Kullanıcının Authenticator uygulamasına erişim sağlayamadığı durumlarda hesabına giriş yapabilmesi için 5 adet kurtarma şifresi (bu sayıyı biz belirliyoruz) GenerateNewTwoFactorRecoveryCodesAsync methoduyla kullanıcıya dönülmektedir.
2FA Login
Kullanıcıları sistemimize login etmeden bir takım değişiklikler yapmamız gerekmektedir.
var user = await _userManager.FindByEmailAsync(viewModel.Email);
if (user != null)
{
await _signInManager.SignOutAsync();
var result = await _signInManager.PasswordSignInAsync(user, viewModel.Password, viewModel.RememberMe, true);
if (result.Succeeded)
{
// OK
}
else if (result.RequiresTwoFactor)
{
return RedirectToAction("TwoFactorLogin");
}
else
{
// Error handling
}
}
Kullanıcı PasswordSignInAsync methoduyla giriş yapmaya çalışıyor. SignInResult içerisinde RequiresTwoFactor property’si true geldiğinde kullanıcı ikinci giriş ekranına yönlendirilmektedir.
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user.TwoFactorType == Models.TwoFactorType.Authenticator)
{
var result = vieWModel.IsRecoveryCode ? await _signInManager.TwoFactorRecoveryCodeSignInAsync(vieWModel.VerificationCode)
: await _signInManager.TwoFactorAuthenticatorSignInAsync(vieWModel.VerificationCode, true, false);
if (result.Succeeded)
{
// OK
}
}
else if (user.TwoFactorType == Models.TwoFactorType.Email || user.TwoFactorType == Models.TwoFactorType.Sms)
{
// Control verificationCode with yours and sign-in
}
Çift aşamalı doğrulama yapmaya çalışan kullanıcı cookie üzerinden GetTwoFactorAuthenticationUserAsync methoduyla elde ediliyor. Sonrasındaysa kullanıcının kurtarma şifresi girip girmediğini kontrol ediliyor. Kullanıcı kurtarma şifresiyle giriş yapmaya çalışıyorsa TwoFactorRecoveryCodeSignInAsync methodu, Authenticator uygulamasından alınan kodu girmişse TwoFactorAuthenticatorSignInAsync methoduyla sisteme giriş yapmaktadır.
2FA doğrulama yönteminde e-posta ve sms kullanılmışsa SignInResult.RequiresTwoFactor property’si true olduğunda kullanıcıya bir kod üretip göndermek gerekmektedir. Sonraki akışta kullanıcının girdiği kod kontrol edilerek SignInAsync methoduyla giriş yaptırılmalıdır.
Identity API Social Logins
ASP.NET Core uygulamalarımız Identiy API sayesinde OAuth 2.0 protokolünü kullanarak kullanıcıların external authentication provider (Facebook, Google, Twitter, Microsoft gibi) credential bilgileriyle oturum açmasını sağlar.
Bu yazıda external authentication provider olarak Facebook, Microsoft ve Google kullanılacaktır. Yazının uzamaması adına ilgili platformlardan ClientId ve ClientSecret bilgilerinin nasıl alındığına değinilmeyecektir. Kullanmak istediğimiz external login provider‘a göre aşağıdaki ayarların yapılması gerekmektedir.
- Facebook kullanılacaksa credential bilgileri alınırken RedirectUrl /signin-facebook olarak belirtilmelidir. Bu ve diğer URL‘ler otomatik oluşturulmaktadır. Özelleştirmek için FacebookOptions property’sinin CallbackPath property’si değiştirilmelidir. Microsoft.AspNetCore.Authentication.Facebook paketi kurulmalıdır.
- Google için RedirectUrl /signin-google olarak belirtilmelidir. GoogleOptions property’si üzerinden değiştirilebilir. Microsoft.AspNetCore.Authentication.Google paketi kurulmalıdır.
- Microsoft için RedirectUrl /signin-microsoft olarak belirtilmelidir. MicrosoftAccountOptions property’si üzerinden değiştirilebilir. Microsoft.AspNetCore.Authentication.MicrosoftAccount paketi kurulmalıdır.
İlgili platformlardan edindiğimiz ClientId ve ClientSecret bilgilerini AddAuthentication methoduyla ekleyelim.
builder.Services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = builder.Configuration.GetValue<string>("ExternalLoginProviders:Facebook:AppId");
options.AppSecret = builder.Configuration.GetValue<string>("ExternalLoginProviders:Facebook:AppSecret");
// options.CallbackPath = new PathString("/User/FacebookCallback");
})
.AddGoogle(options =>
{
options.ClientId = builder.Configuration.GetValue<string>("ExternalLoginProviders:Google:ClientId");
options.ClientSecret = builder.Configuration.GetValue<string>("ExternalLoginProviders:Google:ClientSecret");
})
.AddMicrosoftAccount(options =>
{
options.ClientId = builder.Configuration.GetValue<string>("ExternalLoginProviders:Microsoft:ClientId");
options.ClientSecret = builder.Configuration.GetValue<string>("ExternalLoginProviders:Microsoft:ClientSecret");
});
Kullanılacak provider‘lar AddFacebook, AddGoogle ve AddMicrosoftAccount methodlarıyla eklenmiş ve credential bilgileri verilmiştir.
public IActionResult FacebookSignIn(string returnUrl)
{
var redirectUrl = Url.Action("ExternalResponse", "User", new { ReturnUrl = returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties("Facebook", redirectUrl);
return new ChallengeResult("Facebook", properties);
}
public IActionResult GoogleSignIn(string returnUrl)
{
var redirectUrl = Url.Action("ExternalResponse", "User", new { ReturnUrl = returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties("Google", redirectUrl);
return new ChallengeResult("Google", properties);
}
public IActionResult MicrosoftSignIn(string returnUrl)
{
var redirectUrl = Url.Action("ExternalResponse", "User", new { ReturnUrl = returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties("Microsoft", redirectUrl);
return new ChallengeResult("Microsoft", properties);
}
ConfigureExternalAuthenticationProperties methodlarıyla ReturnUrl ve User Identifier yapılandırılarak Facebook, Google ve Microsoft OAuth URL‘lerine yönlendirme işlemi yapılmaktadır.
public async Task<IActionResult> ExternalResponse(string ReturnUrl = "/")
{
var loginInfo = await _signInManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
var externalLoginResult = await _signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, true);
if (externalLoginResult.Succeeded)
{
return Redirect(ReturnUrl);
}
var externalUserId = loginInfo.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var externalEmail = loginInfo.Principal.FindFirst(ClaimTypes.Email)?.Value;
var existUser = await _userManager.FindByEmailAsync(externalEmail);
if (existUser == null)
{
var user = new AppUser { Email = externalEmail };
if (loginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Name))
{
var userName = loginInfo.Principal.FindFirst(ClaimTypes.Name)?.Value;
if (userName != null)
{
userName = userName.Replace(' ', '-').ToLower() + externalUserId?.Substring(0, 5);
user.UserName = userName;
}
else
{
user.UserName = user.Email;
}
}
else
{
user.UserName = user.Email;
}
var createResult = await _userManager.CreateAsync(user);
if (createResult.Succeeded)
{
var loginResult = await _userManager.AddLoginAsync(user, loginInfo);
if (loginResult.Succeeded)
{
await _signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, true);
return Redirect(ReturnUrl);
}
else
{
loginResult.Errors.ToList().ForEach(f => ModelState.AddModelError(string.Empty, f.Description));
}
}
else
{
createResult.Errors.ToList().ForEach(f => ModelState.AddModelError(string.Empty, f.Description));
}
}
else
{
var loginResult = await _userManager.AddLoginAsync(existUser, loginInfo);
if (loginResult.Succeeded)
{
// await SignInManager.SignInAsync(user, true);
await _signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, true);
return Redirect(ReturnUrl);
}
else
{
loginResult.Errors.ToList().ForEach(f => ModelState.AddModelError(string.Empty, f.Description));
}
}
var errors = ModelState.Values.SelectMany(s => s.Errors).Select(s => s.ErrorMessage).ToList();
return View("Error", errors);
}
OAuth yönlendirme işlemi tamamlandığında tüm provider’lar bu methoda düşecektir. İlgili kullanıcı bilgileri alınarak giriş yaptırılmakta ve kullanıcı mevcut değilse yeni bir kullanıcı oluşturularak giriş işlemi yapılmaktadır. AddLoginAsync methoduyla external login bilgisi AspNetUserLogins tablosuna eklenmektedir.
Geliştirilen proje dosyalarına buradan ulaşabilirsiniz.
Hello,
many thanks for that great article. It helps me a lot! Maybe you want to take a look what I’ve done based on your article.
https://github.com/madcoda9000/dotnet-cookie-based-identity
Best Regards
sascha
Hello Sascha,
I have looked at your repo, and it looks great!
Hocam iki farklı kullanıcı türü için bu kütüphaneyi nasıl kullanabiliriz? mesela Kariyer.net deki aday girişi işveren girişi gib
Samet bunun için farklı şemalar tanımlamalısın.