Asp.NET Core Identity – Şifremi Unuttum – XI
Merhaba,
Asp.NET Core Identity yazı dizimizin 11. makalesinde web sitemize üye olan kullanıcılar tarafından belirtilen şifrelerin -insanlık hali- unutulması durumunda kullanıcının nasıl yeni şifre talep edeceğini Identity mekanizması üzerinden inceleyeceğiz.
Süreçte bütünlüğü sağlayabilmek adına yazı dizimizde kullandığımız projemiz üzerinde geliştirme yapacağız.
Herşeyden önce uygulamada şifremi unuttum mekanizmasını inşa edebilmek için token provider yapılanmasını servis olarak dahil etmemiz gerekmektedir. Bunun için “Startup.cs” dosyasına giderek 21. satırda olduğu gibi “AddDefaultTokenProviders” metodunu eklememiz yeterlidir.
public class Startup { public IConfiguration Configuration { get; set; } public Startup(IConfiguration configuration) => Configuration = configuration; public void ConfigureServices(IServiceCollection services) { services.AddDbContext<AppDbContext>(_ => _.UseSqlServer(Configuration["ConnectionStrings:SqlServerConnectionString"])); services.AddIdentity<AppUser, AppRole>(_ => { _.Password.RequiredLength = 5; //En az kaç karakterli olması gerektiğini belirtiyoruz. _.Password.RequireNonAlphanumeric = false; //Alfanumerik zorunluluğunu kaldırıyoruz. _.Password.RequireLowercase = false; //Küçük harf zorunluluğunu kaldırıyoruz. _.Password.RequireUppercase = false; //Büyük harf zorunluluğunu kaldırıyoruz. _.Password.RequireDigit = false; //0-9 arası sayısal karakter zorunluluğunu kaldırıyoruz. _.User.RequireUniqueEmail = true; //Email adreslerini tekilleştiriyoruz. _.User.AllowedUserNameCharacters = "abcçdefghiıjklmnoöpqrsştuüvwxyzABCÇDEFGHIİJKLMNOÖPQRSŞTUÜVWXYZ0123456789-._@+"; //Kullanıcı adında geçerli olan karakterleri belirtiyoruz. }).AddPasswordValidator<CustomPasswordValidation>() .AddUserValidator<CustomUserValidation>() .AddErrorDescriber<CustomIdentityErrorDescriber>().AddEntityFrameworkStores<AppDbContext>() .AddDefaultTokenProviders(); services.ConfigureApplicationCookie(_ => { _.LoginPath = new PathString("/User/Login"); _.Cookie = new CookieBuilder { Name = "AspNetCoreIdentityExampleCookie", //Oluşturulacak Cookie'yi isimlendiriyoruz. HttpOnly = false, //Kötü niyetli insanların client-side tarafından Cookie'ye erişmesini engelliyoruz. Expiration = TimeSpan.FromMinutes(2), //Oluşturulacak Cookie'nin vadesini belirliyoruz. SameSite = SameSiteMode.Lax, //Top level navigasyonlara sebep olmayan requestlere Cookie'nin gönderilmemesini belirtiyoruz. SecurePolicy = CookieSecurePolicy.Always //HTTPS üzerinden erişilebilir yapıyoruz. }; _.SlidingExpiration = true; //Expiration süresinin yarısı kadar süre zarfında istekte bulunulursa eğer geri kalan yarısını tekrar sıfırlayarak ilk ayarlanan süreyi tazeleyecektir. _.ExpireTimeSpan = TimeSpan.FromMinutes(2); //CookieBuilder nesnesinde tanımlanan Expiration değerinin varsayılan değerlerle ezilme ihtimaline karşın tekrardan Cookie vadesi burada da belirtiliyor. }); services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseAuthentication(); app.UseMvc(_ => _.MapRoute("Default", "{controller=Home}/{action=Index}/{id?}")); } }
Bu temel konfigürasyondan sonra kullanıcının ihtiyaca binaen yeniden şifre talep edebilmesi için “Şifremi Unuttum” sayfasını tasarlayalım.
Şifremi Unuttum Sayfasının Tasarlanması
İlk olarak uygulamamızın “Models” -> “ViewModels” klasörü altında yeni şifre talebi esnasında veriyi karşılayabilmek için “ResetPasswordViewModel” isminde bir viewmodel tasarlıyoruz.
public class ResetPasswordViewModel { [Display(Name = "E-Posta Adresiniz")] [Required(ErrorMessage = "Lütfen e-posta adresinizi boş geçmeyiniz.")] [EmailAddress(ErrorMessage = "Lütfen uygun formatta e-posta giriniz.")] public string Email { get; set; } }
Ardından “User(Controller).cs” controller sınıfı içerisinde ilgili talebi yöneteceğimiz “PasswordReset” isimli bir action oluşturuyoruz ve aşağıdaki gibi geliştiriyoruz.
public class UserController : Controller { readonly UserManager<AppUser> _userManager; readonly SignInManager<AppUser> _signInManager; public UserController(UserManager<AppUser> userManager, SignInManager<AppUser> signInManager) { _userManager = userManager; _signInManager = signInManager; } public IActionResult PasswordReset() { return View(); } [HttpPost] public async Task<IActionResult> PasswordReset(ResetPasswordViewModel model) { AppUser user = await _userManager.FindByEmailAsync(model.Email); if (user != null) { string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user); MailMessage mail = new MailMessage(); mail.IsBodyHtml = true; mail.To.Add(user.Email); mail.From = new MailAddress("******@gmail.com", "Şifre Güncelleme", System.Text.Encoding.UTF8); mail.Subject = "Şifre Güncelleme Talebi"; mail.Body = $"<a target=\"_blank\" href=\"https://localhost:5001{Url.Action("UpdatePassword", "User", new { userId = user.Id, token = HttpUtility.UrlEncode(resetToken) })}\">Yeni şifre talebi için tıklayınız</a>"; mail.IsBodyHtml = true; SmtpClient smp = new SmtpClient(); smp.Credentials = new NetworkCredential("*****@gmail.com", "******"); smp.Port = 587; smp.Host = "smtp.gmail.com"; smp.EnableSsl = true; smp.Send(mail); ViewBag.State = true; } else ViewBag.State = false; return View(); } . . . //diğer actionlar . . . }
Yukarıdaki kod bloğunda, birazdan view içeriğinide göreceğimiz “PasswordReset” actionı geliştirilmiştir. Yapılan post neticesinde kullanıcı tarafından doğru email adresi girilmişse eğer 20. satırda “GeneratePasswordResetTokenAsync” metodu ile kullanıcıya özel token değeri üretilmektedir. Hemen ardından üretilen bu değer user id değeri ile birlikte oluşturulan bir update linkine query string olarak eklenmekte ve ilgili url yeni şifre talep eden kullanıcının email adresine gönderilmektedir. Dolayısıyla kullanıcı url üzerinden gönderilen token değerinin güvencesiyle kendisine ait hesaba dair şifreyi güncelleyebilecektir.
Evet… Burada email yapılanmasını farklı bir katmanda tasarlamak daha doğru olabilir. Lakin örneklendirmeyi fazla parçalamamak adına olması gereken bu durumu sizlere bırakıyorum…
Şimdide “PasswordReset” actionının view’ini görelim.
@model AspNetCoreIdentityExample.Models.ViewModels.ResetPasswordViewModel @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers <h3>Şifremi Unuttum</h3> <hr /> @if (ViewBag.State == null) { <form asp-action="PasswordReset"> <table> <tr> <td colspan="3"><div asp-validation-summary="All"></div></td> </tr> <tr> <td><label asp-for="Email"></label></td> <td><input asp-for="Email" /></td> <td><span asp-validation-for="Email"></span></td> </tr> <tr> <td colspan="3"><input type="submit" value="Yeni Şifre Talebi Gönder" /></td> </tr> </table> </form> } else { if (ViewBag.State) { @:<div class="alert-success">Şifre talebi için e-posta adresinize bilgilerindirici mail gönderilmiştir.</div> } else { @:<div class="alert-success">Böyle bir e-posta bulunmamaktadır.</div> } }
Evet, artık şifreyi güncelleme işlemini operasyonel olarak gerçekleştirecek “UpdatePassword” action metodunu tasarlayabiliriz.
Şifreyi Güncelleme Sayfasının Tasarlanması
Kullanıcı tarafımızca mail üzerinden gönderilen url’e tıkladığı vakit yeni şifresini belirleyebilmek için “UpdatePassword” action metodu tarafından karşılanacaktır. Dolayısıyla ilk olarak kullanıcının girdiği şifreyi taşıması için “UpdatePasswordViewModel” isminde bir viewmodel tasarlayalım.
public class UpdatePasswordViewModel { [Display(Name = "Yeni Şifre")] [Required(ErrorMessage = "Lütfen şifreyi boş geçmeyiniz.")] [DataType(DataType.Password)] public string Password { get; set; } }
Ardından “UpdatePassword” action metodunu inşa edelim.
public class UserController : Controller { readonly UserManager<AppUser> _userManager; readonly SignInManager<AppUser> _signInManager; public UserController(UserManager<AppUser> userManager, SignInManager<AppUser> signInManager) { _userManager = userManager; _signInManager = signInManager; } [HttpGet("[action]/{userId}/{token}")] public IActionResult UpdatePassword(string userId, string token) { return View(); } [HttpPost("[action]/{userId}/{token}")] public async Task<IActionResult> UpdatePassword(UpdatePasswordViewModel model, string userId, string token) { AppUser user = await _userManager.FindByIdAsync(userId); IdentityResult result = await _userManager.ResetPasswordAsync(user, HttpUtility.UrlDecode(token), model.Password); if (result.Succeeded) { ViewBag.State = true; await _userManager.UpdateSecurityStampAsync(user); } else ViewBag.State = false; return View(); } } . . . //diğer actionlar . . .
Görüldüğü üzere kullanıcıya gönderilen e-postadaki urle tıklandığı vakit query string olarak belirtilen userId ve token değerleri “UpdatePassword” action metodu tarafından yakalanmakta ve TempData‘ya atılmaktadır. Post neticesinde “UpdatePasswordViewModel” parametresi kullanıcı tarafından girilen yeni şifreyi taşımaktadır ve 19. satırda olduğu gibi “ResetPasswordAsync” metodu ile token değeri kontrolünde şifre güncellemesi gerçekleştirilmektedir. 23. satırda ise ana değerlerden biri olan şifrenin değişmesinden dolayı “UpdateSecurityStampAsync” metodu aracılığıyla ilgili kullanıcının SecurityStamp kolonu yeni bir değerle güncellenmiştir. Böylece önceki üretilen cookie değerleri çürütülmüştür.
“UpdatePassword” actionının view dosyasınıda incelersek eğer;
@model AspNetCoreIdentityExample.Models.ViewModels.UpdatePasswordViewModel @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers <h3>Şifremi Unuttum</h3> <hr /> @if (ViewBag.State == null) { <form asp-action="UpdatePassword"> <table> <tr> <td colspan="3"><div asp-validation-summary="All"></div></td> </tr> <tr> <td><label asp-for="Password"></label></td> <td><input asp-for="Password" /></td> <td><span asp-validation-for="Password"></span></td> </tr> <tr> <td colspan="3"><input type="submit" value="Şifreyi Güncelle" /></td> </tr> </table> </form> } else { if (ViewBag.State) { @:<div class="alert-success">Şifre başarıyla güncellenmiştir.</div> } else { @:<div class="alert-success">Şifreyi güncellerken beklenmeyen hatayla karşılaşıldı.</div> } }
şeklinde olacaktır.
Artık uygulamanın bu son halini kaydedip, derlediğiniz vakit aşağıdaki gibi şifremi unuttum mekanizmasının çalıştığını gözlemleyebilirsiniz.
İ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.
Merhaba Hocam,
Şifre yenileme talebi için e-postaya gelen mail’e tıkladığımda
” HTTP Error 404.11 – Not Found
İstek filtreleme modülü çift kaçış sırası içeren bir isteği reddedecek şekilde yapılandırıldı.” hatası alıyorum.
Nasıl düzeltebilirim.
Merhaba,
Mail ile gönderdiğin url’in generate edildiği kodu paylaşır mısın?
hocam size gonderdiğim kod engelleniyor heralde
Sanırım olabilir… Ekran görüntüsünü alıp burada paylaşınız.
Görüntü kabul etmiyor bu alan.
Mail atmıştım size ordan bakabilir misiniz?
Mailiniz gelmedi.
https://tr.imgbb.com/
adresini kullanarak resim yükleyebilir ve verilen linki buradan paylaşabilirsiniz.
Peki… Şimdi bana oluşturulan örnek bir url gönderebilir misiniz?
Ayrıca String Interpolation kullandığınız string işlemlerinde ekstradan + operatörü ile birleştirme yapmamanızı öneririm.
Size gönderdiğim fotoğrafa sığması için böyle yazmıştım. Normalde kod tek satırda yazılı. Örnek URL’e burdan ulaşabilirsiniz:
https://ibb.co/X8zHQ39
Url’i görsel olarak tam görememekteyim. Lütfen metin olarak paylaşınız. Ya da siz URL’de özellikle “+” operatörü ya da benzeri var mı kontrol ediniz. Hatanın sebebi url’de ki bu tarz bir karakterin olması olabilir.
Url’de harf, rakam ve % işareti dışında herhangi bir karakter bulunmuyor.
Uygulamayı IIS üzerinden değilde kendi hostu üzerinden çalıştırmayı denediniz mi?
Bu şekilde oldu hocam. IIS’le çalışmasını nasıl sağlayabilirim?
Gün boyu yaptığım tüm araştırmalar ve denemeler neticesinde bir türlü soruna çözüm bulamadım.
https://localhost:44330/UpdatePassword/94519abf-d22a
-45ed-88ec-c61e3afdc913/CfDJ8Cv5GzmHKPNMjkS%252byklWmBHnf0x
cfJZZ0zfRCM9S2yGZLMUmoeSL2YQSJOJ9uJdjhnAYw0cd35Y8k4
rcEX%252b1VvY09hKlQqLIwMsryPlhNwXfEDmovG6UaC%252fW0rnC
TPlF55C3%252bwE%252f5iPBSDM9GpiibAYh1L%252fn6LpxTXtl4pJqYb
%252fWNJKM360QulZD6IJBVep7PUmIZEfQUro0saLx4Uvhdq2qZqvpuSEtNpqccXR%252fGJ1j
Yardımınız için teşekkürler….
Eğer ki çözüm bulursanız buradan paylaşmanızı rica ediyorum.
Kolay gelsin.
merhaba hocam bu sorunun çözümü için yardımcı olabilir misiniz?
Merhaba,
Hangi sorunun çözümü için yardım istiyorsunuz?
Şifre yenileme talebi için e-postaya gelen mail’e tıkladığımda
” HTTP Error 404.11 – Not Found
İstek filtreleme modülü çift kaçış sırası içeren bir isteği reddedecek şekilde yapılandırıldı.” hatasını bende alıyorum ve bu hatanın oluşan url in token değerinde özel karakterlerin bulunmasından kaynaklı olduğu için hata veriyor fakat bu özel karakterleri nasıl ortadan kaldırırım veya nasıl bu şekilde çalışmasına izin verilir bunu bulamadım.
Sorunun çözümü :
ve
bunlar ile değiştirin.
Teşekkürler.
Ben teşekkür ederim aslında bu güzel bilgileri bizimle paylaştığınız için.
Çok sağol İshak kardeşim 🙂
Hocam aynı hatayı alıyorum bu code decoded ve encoded ları nerede değiştireceğiz hiç anlamadım projede öyle bi kod parçacığım yok malesef
İshak Beyin şuradaki çözümüyle alakalı sorun yaşıyor olabilirsin. İncelemeni öneririm.
Hocam inceledim fakat anlamadigim nokta şurası bu posttaki şekilde yapilandirdim kodumu fakatnencode ve decode yaptigim bir alan yok ishak bey encode ve decode yapiyor nereye ekleyecegimi bilemedim ishak beyin çöźümünü
Berkay bey aynı problemi bende yaşıyorum . Yani posttaki yapıda İshak beyin çözümünü nerede yapacağımızı bulamadım. Yapan varsa bilgilendirebilir mi?
mrhba efendim bu ıdenty kodunun tamamını git de palaşırmısnz bu mailden cevap yazarsanız sevinirim
Merhaba,
Malesef git kullanmıyorum.
Hocam selamlar,
PasswordReset Actionunda smp.send(mail) kısmında hata almaktayım.
hata şu şekilde;
///////
An unhandled exception occurred while processing the request.
SocketException: Bilinen böyle bir ana bilgisayar yok
System.Net.Dns.InternalGetHostByName(string hostName)
SmtpException: Failure sending mail.
System.Net.Mail.SmtpClient.Send(MailMessage message)
/////
ViewBag.state=true da patlıyor. sanırım bunun nedeni SmtpClient in config tarafında sorun var fakat her bilgi doğru yazdığımı düşünüyorum.
Merhaba,
‘smtp.bilisimkampusu.com’ böyle bir sunucun olduğuna emin misin?
Hocam aynı sorun bende de var smtp.gmail.com bende ki ama aynı sorun devam ediyor
Selamün aleyküm
Kolay gelsin başlığı core olarak açmışsınız ama .net frameworkta kodlar çalışıyormu bilginiz varmı?
Merhaba,
Bu yazı serisinde sadece .NET Core’a uygun bir anlatım sergilenmiştir. .NET Framework’te de çalışmaların benzer niteliklerde sergilenebileceği kanaatindeyim.
mail.Body = $”Yeni şifre talebi için tıklayınız”;
Hocam merhaba,
“GeneratePasswordResetTokenAsync” metodu ile oluşturulan token’ınımız (userId, token,expireTime gibi bilgilerle) bir cookie’de tutuluyor değil mi?
İyi çalışmalar.
Merhaba,
İlgili fonksiyon neticesinde SecurityStamp değeriyle doğrulanabilir bir token oluşturuluyor. Haliyle bizler token’ı doğruladıktan sonra ilgili ‘SecurityStamp’ değerini güncelliyoruz.
Sağolun hocam anladım şimdi.
İyi çalışmalar.
else
ViewBag.State = false;
return View();
BURAYA GELİYOR YADA 404.11
Sorunun çözümü :
1
2
byte[] tokenGeneratedBytes = Encoding.UTF8.GetBytes(resetToken);
var codeEncoded = WebEncoders.Base64UrlEncode(tokenGeneratedBytes);
ve
1
2
var codeDecodedBytes = WebEncoders.Base64UrlDecode(UID);
var codeDecoded = Encoding.UTF8.GetString(codeDecodedBytes);
nedir??
Sa Hocam Bir Sorum Var
Buyrun…