From 143f708cffd5da2e4ac54cfa8005ad39c9fb6912 Mon Sep 17 00:00:00 2001 From: andy Date: Sun, 24 Oct 2021 00:23:45 +0100 Subject: [PATCH] scaffolding identity --- Selector.Model/Selector.Model.csproj | 5 +- Selector.Model/SelectorContext.cs | 5 +- .../Areas/Identity/IdentityHostingStartup.cs | 22 ++++ .../Areas/Identity/Pages/Account/Login.cshtml | 85 +++++++++++++ .../Identity/Pages/Account/Login.cshtml.cs | 110 +++++++++++++++++ .../Identity/Pages/Account/Logout.cshtml | 21 ++++ .../Identity/Pages/Account/Logout.cshtml.cs | 43 +++++++ .../Identity/Pages/Account/Register.cshtml | 67 ++++++++++ .../Identity/Pages/Account/Register.cshtml.cs | 114 ++++++++++++++++++ .../Pages/Account/_ViewImports.cshtml | 1 + .../Pages/_ValidationScriptsPartial.cshtml | 18 +++ .../Areas/Identity/Pages/_ViewImports.cshtml | 4 + .../Areas/Identity/Pages/_ViewStart.cshtml | 4 + Selector.Web/Pages/Index.cshtml | 1 + .../Pages/Shared/_LoginPartial.cshtml | 27 +++++ Selector.Web/Selector.Web.csproj | 7 ++ Selector.Web/Startup.cs | 40 ++++++ 17 files changed, 572 insertions(+), 2 deletions(-) create mode 100644 Selector.Web/Areas/Identity/IdentityHostingStartup.cs create mode 100644 Selector.Web/Areas/Identity/Pages/Account/Login.cshtml create mode 100644 Selector.Web/Areas/Identity/Pages/Account/Login.cshtml.cs create mode 100644 Selector.Web/Areas/Identity/Pages/Account/Logout.cshtml create mode 100644 Selector.Web/Areas/Identity/Pages/Account/Logout.cshtml.cs create mode 100644 Selector.Web/Areas/Identity/Pages/Account/Register.cshtml create mode 100644 Selector.Web/Areas/Identity/Pages/Account/Register.cshtml.cs create mode 100644 Selector.Web/Areas/Identity/Pages/Account/_ViewImports.cshtml create mode 100644 Selector.Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml create mode 100644 Selector.Web/Areas/Identity/Pages/_ViewImports.cshtml create mode 100644 Selector.Web/Areas/Identity/Pages/_ViewStart.cshtml create mode 100644 Selector.Web/Pages/Shared/_LoginPartial.cshtml diff --git a/Selector.Model/Selector.Model.csproj b/Selector.Model/Selector.Model.csproj index cb4910c..cb80652 100644 --- a/Selector.Model/Selector.Model.csproj +++ b/Selector.Model/Selector.Model.csproj @@ -11,8 +11,11 @@ + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Selector.Model/SelectorContext.cs b/Selector.Model/SelectorContext.cs index d92e0ad..db82cbc 100644 --- a/Selector.Model/SelectorContext.cs +++ b/Selector.Model/SelectorContext.cs @@ -5,10 +5,12 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; + namespace Selector.Model { - public class SelectorContext : DbContext + public class SelectorContext : IdentityDbContext { public DbSet Watcher { get; set; } @@ -24,6 +26,7 @@ namespace Selector.Model protected override void OnModelCreating(ModelBuilder modelBuilder) { + base.OnModelCreating(modelBuilder); } } diff --git a/Selector.Web/Areas/Identity/IdentityHostingStartup.cs b/Selector.Web/Areas/Identity/IdentityHostingStartup.cs new file mode 100644 index 0000000..1ae64a0 --- /dev/null +++ b/Selector.Web/Areas/Identity/IdentityHostingStartup.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Selector.Model; + +[assembly: HostingStartup(typeof(Selector.Web.Areas.Identity.IdentityHostingStartup))] +namespace Selector.Web.Areas.Identity +{ + public class IdentityHostingStartup : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => { + //services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true); + }); + } + } +} \ No newline at end of file diff --git a/Selector.Web/Areas/Identity/Pages/Account/Login.cshtml b/Selector.Web/Areas/Identity/Pages/Account/Login.cshtml new file mode 100644 index 0000000..72a567f --- /dev/null +++ b/Selector.Web/Areas/Identity/Pages/Account/Login.cshtml @@ -0,0 +1,85 @@ +@page +@model LoginModel + +@{ + ViewData["Title"] = "Log in"; +} + +

@ViewData["Title"]

+
+
+
+
+

Use a local account to log in.

+
+
+
+ + + +
+
+ + + +
+
+
+ +
+
+
+ +
+ +
+
+
+
+
+

Use another service to log in.

+
+ @{ + if ((Model.ExternalLogins?.Count ?? 0) == 0) + { +
+

+ There are no external authentication services configured. See this article + for details on setting up this ASP.NET application to support logging in via external services. +

+
+ } + else + { +
+
+

+ @foreach (var provider in Model.ExternalLogins) + { + + } +

+
+
+ } + } +
+
+
+ +@section Scripts { + +} diff --git a/Selector.Web/Areas/Identity/Pages/Account/Login.cshtml.cs b/Selector.Web/Areas/Identity/Pages/Account/Login.cshtml.cs new file mode 100644 index 0000000..b10fcde --- /dev/null +++ b/Selector.Web/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Selector.Web.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginModel(SignInManager signInManager, + ILogger logger, + UserManager userManager) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public IList ExternalLogins { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public async Task OnGetAsync(string returnUrl = null) + { + if (!string.IsNullOrEmpty(ErrorMessage)) + { + ModelState.AddModelError(string.Empty, ErrorMessage); + } + + returnUrl ??= Url.Content("~/"); + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + + ReturnUrl = returnUrl; + } + + public async Task OnPostAsync(string returnUrl = null) + { + returnUrl ??= Url.Content("~/"); + + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + + if (ModelState.IsValid) + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); + if (result.Succeeded) + { + _logger.LogInformation("User logged in."); + return LocalRedirect(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User account locked out."); + return RedirectToPage("./Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return Page(); + } + } + + // If we got this far, something failed, redisplay form + return Page(); + } + } +} diff --git a/Selector.Web/Areas/Identity/Pages/Account/Logout.cshtml b/Selector.Web/Areas/Identity/Pages/Account/Logout.cshtml new file mode 100644 index 0000000..eca33c6 --- /dev/null +++ b/Selector.Web/Areas/Identity/Pages/Account/Logout.cshtml @@ -0,0 +1,21 @@ +@page +@model LogoutModel +@{ + ViewData["Title"] = "Log out"; +} + +
+

@ViewData["Title"]

+ @{ + if (User.Identity.IsAuthenticated) + { +
+ +
+ } + else + { +

You have successfully logged out of the application.

+ } + } +
\ No newline at end of file diff --git a/Selector.Web/Areas/Identity/Pages/Account/Logout.cshtml.cs b/Selector.Web/Areas/Identity/Pages/Account/Logout.cshtml.cs new file mode 100644 index 0000000..05c41c6 --- /dev/null +++ b/Selector.Web/Areas/Identity/Pages/Account/Logout.cshtml.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Selector.Web.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LogoutModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LogoutModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + public void OnGet() + { + } + + public async Task OnPost(string returnUrl = null) + { + await _signInManager.SignOutAsync(); + _logger.LogInformation("User logged out."); + if (returnUrl != null) + { + return LocalRedirect(returnUrl); + } + else + { + return RedirectToPage(); + } + } + } +} diff --git a/Selector.Web/Areas/Identity/Pages/Account/Register.cshtml b/Selector.Web/Areas/Identity/Pages/Account/Register.cshtml new file mode 100644 index 0000000..96e6a9a --- /dev/null +++ b/Selector.Web/Areas/Identity/Pages/Account/Register.cshtml @@ -0,0 +1,67 @@ +@page +@model RegisterModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+ +
+
+
+

Create a new account.

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+
+

Use another service to register.

+
+ @{ + if ((Model.ExternalLogins?.Count ?? 0) == 0) + { +
+

+ There are no external authentication services configured. See this article + for details on setting up this ASP.NET application to support logging in via external services. +

+
+ } + else + { +
+
+

+ @foreach (var provider in Model.ExternalLogins) + { + + } +

+
+
+ } + } +
+
+
+ +@section Scripts { + +} diff --git a/Selector.Web/Areas/Identity/Pages/Account/Register.cshtml.cs b/Selector.Web/Areas/Identity/Pages/Account/Register.cshtml.cs new file mode 100644 index 0000000..524f6dc --- /dev/null +++ b/Selector.Web/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; + +namespace Selector.Web.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class RegisterModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly IEmailSender _emailSender; + + public RegisterModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger, + IEmailSender emailSender) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + _emailSender = emailSender; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string ReturnUrl { get; set; } + + public IList ExternalLogins { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [Required] + //[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public async Task OnGetAsync(string returnUrl = null) + { + ReturnUrl = returnUrl; + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + } + + public async Task OnPostAsync(string returnUrl = null) + { + returnUrl ??= Url.Content("~/"); + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + if (ModelState.IsValid) + { + var user = new IdentityUser { UserName = Input.Email, Email = Input.Email }; + var result = await _userManager.CreateAsync(user, Input.Password); + if (result.Succeeded) + { + _logger.LogInformation("User created a new account with password."); + + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = Url.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl }, + protocol: Request.Scheme); + + await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", + $"Please confirm your account by clicking here."); + + if (_userManager.Options.SignIn.RequireConfirmedAccount) + { + return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl }); + } + else + { + await _signInManager.SignInAsync(user, isPersistent: false); + return LocalRedirect(returnUrl); + } + } + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + // If we got this far, something failed, redisplay form + return Page(); + } + } +} diff --git a/Selector.Web/Areas/Identity/Pages/Account/_ViewImports.cshtml b/Selector.Web/Areas/Identity/Pages/Account/_ViewImports.cshtml new file mode 100644 index 0000000..fc36f7d --- /dev/null +++ b/Selector.Web/Areas/Identity/Pages/Account/_ViewImports.cshtml @@ -0,0 +1 @@ +@using Selector.Web.Areas.Identity.Pages.Account \ No newline at end of file diff --git a/Selector.Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml b/Selector.Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..9e26f3b --- /dev/null +++ b/Selector.Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/Selector.Web/Areas/Identity/Pages/_ViewImports.cshtml b/Selector.Web/Areas/Identity/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..777ce50 --- /dev/null +++ b/Selector.Web/Areas/Identity/Pages/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Identity +@using Selector.Web.Areas.Identity +@using Selector.Web.Areas.Identity.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Selector.Web/Areas/Identity/Pages/_ViewStart.cshtml b/Selector.Web/Areas/Identity/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..2909abd --- /dev/null +++ b/Selector.Web/Areas/Identity/Pages/_ViewStart.cshtml @@ -0,0 +1,4 @@ + +@{ + Layout = "/Pages/Shared/_Layout.cshtml"; +} diff --git a/Selector.Web/Pages/Index.cshtml b/Selector.Web/Pages/Index.cshtml index 388b71f..dc093d0 100644 --- a/Selector.Web/Pages/Index.cshtml +++ b/Selector.Web/Pages/Index.cshtml @@ -7,6 +7,7 @@

Welcome

Learn about building Web apps with ASP.NET Core.

+
@section Scripts { diff --git a/Selector.Web/Pages/Shared/_LoginPartial.cshtml b/Selector.Web/Pages/Shared/_LoginPartial.cshtml new file mode 100644 index 0000000..9f617b3 --- /dev/null +++ b/Selector.Web/Pages/Shared/_LoginPartial.cshtml @@ -0,0 +1,27 @@ +@using Microsoft.AspNetCore.Identity + +@inject SignInManager SignInManager +@inject UserManager UserManager + + diff --git a/Selector.Web/Selector.Web.csproj b/Selector.Web/Selector.Web.csproj index 029b3a1..1130f3a 100644 --- a/Selector.Web/Selector.Web.csproj +++ b/Selector.Web/Selector.Web.csproj @@ -12,9 +12,16 @@
+ + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Selector.Web/Startup.cs b/Selector.Web/Startup.cs index ba9401b..225fa3f 100644 --- a/Selector.Web/Startup.cs +++ b/Selector.Web/Startup.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Selector.Model; @@ -33,6 +34,44 @@ namespace Selector.Web services.AddDbContext(options => options.UseNpgsql(Configuration.GetConnectionString("Default")) ); + + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultUI() + .AddDefaultTokenProviders(); + + services.Configure(options => + { + // Password settings. + //options.Password.RequireDigit = true; + //options.Password.RequireLowercase = true; + //options.Password.RequireNonAlphanumeric = true; + //options.Password.RequireUppercase = true; + options.Password.RequiredLength = 3; + options.Password.RequiredUniqueChars = 1; + + // Lockout settings. + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); + options.Lockout.MaxFailedAccessAttempts = 5; + options.Lockout.AllowedForNewUsers = true; + + // User settings. + options.User.AllowedUserNameCharacters = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + options.User.RequireUniqueEmail = false; + options.SignIn.RequireConfirmedEmail = false; + }); + + services.ConfigureApplicationCookie(options => + { + // Cookie settings + options.Cookie.HttpOnly = true; + options.ExpireTimeSpan = TimeSpan.FromMinutes(5); + + options.LoginPath = "/Identity/Account/Login"; + options.AccessDeniedPath = "/Identity/Account/AccessDenied"; + options.SlidingExpiration = true; + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -54,6 +93,7 @@ namespace Selector.Web app.UseRouting(); + app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints =>