diff --git a/RavenDB.Identity.sln b/RavenDB.Identity.sln index bd57aaf..9187a7c 100644 --- a/RavenDB.Identity.sln +++ b/RavenDB.Identity.sln @@ -10,11 +10,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{DCA799 .nuget\NuGet.targets = .nuget\NuGet.targets EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{B730B0AD-FB9B-4768-BC78-B099DD77A876}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RavenDB.Identity", "RavenDB.Identity\RavenDB.Identity.csproj", "{C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "Sample\Sample.csproj", "{BAF1679A-0D3F-4373-B3E4-7C1C78EFF3E2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RavenDB.Identity", "RavenDB.Identity\RavenDB.Identity.csproj", "{C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "Sample\Sample.csproj", "{35105AF1-C737-4F5B-BF31-1889100BE511}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -22,21 +20,18 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {BAF1679A-0D3F-4373-B3E4-7C1C78EFF3E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BAF1679A-0D3F-4373-B3E4-7C1C78EFF3E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BAF1679A-0D3F-4373-B3E4-7C1C78EFF3E2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BAF1679A-0D3F-4373-B3E4-7C1C78EFF3E2}.Release|Any CPU.Build.0 = Release|Any CPU {C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}.Release|Any CPU.Build.0 = Release|Any CPU + {35105AF1-C737-4F5B-BF31-1889100BE511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35105AF1-C737-4F5B-BF31-1889100BE511}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35105AF1-C737-4F5B-BF31-1889100BE511}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35105AF1-C737-4F5B-BF31-1889100BE511}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {BAF1679A-0D3F-4373-B3E4-7C1C78EFF3E2} = {B730B0AD-FB9B-4768-BC78-B099DD77A876} - EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {10F0E164-7506-4DA6-92BE-EA5E531FC11D} EndGlobalSection diff --git a/RavenDB.Identity/RavenDB.Identity.csproj b/RavenDB.Identity/RavenDB.Identity.csproj index 4694376..f43b5bc 100644 --- a/RavenDB.Identity/RavenDB.Identity.csproj +++ b/RavenDB.Identity/RavenDB.Identity.csproj @@ -3,11 +3,11 @@ The simple and easy RavenDB Identity provider for ASP.NET Core. Use Raven to store your users and logins. As simple as services.AddRavenDbIdentity<AppUser>(); Judah Gabriel Himango - netstandard1.6 + netstandard2.0 true - RavenDb.Identity + RavenDB.Identity RavenDB Identity for ASP.NET Core - RavenDb.Identity + RavenDB.Identity ravendb;identity;aspnetcore;membership;authentication;nosql;authorization https://github.com/JudahGabriel/RavenDB.Identity git @@ -18,14 +18,14 @@ false false false - 1.0.3 + 2.0.2 False - - - + + + diff --git a/RavenDB.Identity/RoleStore.cs b/RavenDB.Identity/RoleStore.cs index d2ba0dc..68b4db1 100644 --- a/RavenDB.Identity/RoleStore.cs +++ b/RavenDB.Identity/RoleStore.cs @@ -13,10 +13,24 @@ using Microsoft.Extensions.Logging; namespace RavenDB.Identity { + /// + /// + /// + /// public class RoleMan : RoleManager where TRole : class { - public RoleMan(IRoleStore store, IEnumerable> roleValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, ILogger> logger, IHttpContextAccessor contextAccessor) : base(store, roleValidators, keyNormalizer, errors, logger, contextAccessor) + /// + /// + /// + /// + /// + /// + /// + /// + /// + public RoleMan(IRoleStore store, IEnumerable> roleValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, ILogger> logger, IHttpContextAccessor contextAccessor) : + base(store, roleValidators, keyNormalizer, errors, logger) { } } diff --git a/RavenDB.Identity/ServiceCollectionExtensions.cs b/RavenDB.Identity/ServiceCollectionExtensions.cs index 4c3f0de..9c5b6c4 100644 --- a/RavenDB.Identity/ServiceCollectionExtensions.cs +++ b/RavenDB.Identity/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using Raven.Client; using Raven.Client.Document; using System; @@ -84,10 +85,13 @@ namespace RavenDB.Identity public static IServiceCollection AddRavenDbIdentity(this IServiceCollection services) where TUser : IdentityUser { - services.AddIdentity(); // Adds the AspNet identity system to work with our RavenDB identity objects. + // Add the AspNet identity system to work with our RavenDB identity objects. + services.AddIdentity() + .AddDefaultTokenProviders(); // options => options.Tokens.ProviderMap.Add("Default", new TokenProviderDescriptor(typeof(UserStore))) services.AddScoped, UserStore>(); services.AddScoped, RoleStore>(); + return services; } } diff --git a/Sample/Controllers/AccountController.cs b/Sample/Controllers/AccountController.cs index a271f8b..5291562 100644 --- a/Sample/Controllers/AccountController.cs +++ b/Sample/Controllers/AccountController.cs @@ -3,62 +3,57 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Sample.Web.NetCore.Models; -using Sample.Web.NetCore.Models.AccountViewModels; -using Sample.Web.NetCore.Services; +using Sample.Models; +using Sample.Models.AccountViewModels; +using Sample.Services; using Raven.Client; -namespace Sample.Web.NetCore.Controllers +namespace Sample.Controllers { [Authorize] + [Route("[controller]/[action]")] public class AccountController : RavenController { private readonly UserManager _userManager; private readonly SignInManager _signInManager; private readonly IEmailSender _emailSender; - private readonly ISmsSender _smsSender; private readonly ILogger _logger; - private readonly string _externalCookieScheme; public AccountController( UserManager userManager, SignInManager signInManager, - IOptions identityCookieOptions, IEmailSender emailSender, - ISmsSender smsSender, - ILoggerFactory loggerFactory, + ILogger logger, IAsyncDocumentSession dbSession) // Grab the RavenDB document session so that we can pass it to our base RavenController class. - : base(dbSession) // Pass the RavenDB doc session to the base controller. Now we can access Raven via .DbSession. + : base(dbSession) // Pass the RavenDB doc session to the base controller. We can access the Raven session via this.DbSession { _userManager = userManager; _signInManager = signInManager; - _externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme; _emailSender = emailSender; - _smsSender = smsSender; - _logger = loggerFactory.CreateLogger(); + _logger = logger; } - // - // GET: /Account/Login + [TempData] + public string ErrorMessage { get; set; } + [HttpGet] [AllowAnonymous] public async Task Login(string returnUrl = null) { // Clear the existing external cookie to ensure a clean login process - await HttpContext.Authentication.SignOutAsync(_externalCookieScheme); + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); ViewData["ReturnUrl"] = returnUrl; return View(); } - // - // POST: /Account/Login [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] @@ -72,17 +67,17 @@ namespace Sample.Web.NetCore.Controllers var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false); if (result.Succeeded) { - _logger.LogInformation(1, "User logged in."); + _logger.LogInformation("User logged in."); return RedirectToLocal(returnUrl); } if (result.RequiresTwoFactor) { - return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); + return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe }); } if (result.IsLockedOut) { - _logger.LogWarning(2, "User account locked out."); - return View("Lockout"); + _logger.LogWarning("User account locked out."); + return RedirectToAction(nameof(Lockout)); } else { @@ -95,8 +90,123 @@ namespace Sample.Web.NetCore.Controllers return View(model); } - // - // GET: /Account/Register + [HttpGet] + [AllowAnonymous] + public async Task LoginWith2fa(bool rememberMe, string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + + if (user == null) + { + throw new ApplicationException($"Unable to load two-factor authentication user."); + } + + var model = new LoginWith2faViewModel { RememberMe = rememberMe }; + ViewData["ReturnUrl"] = returnUrl; + + return View(model); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty); + + var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id); + return RedirectToLocal(returnUrl); + } + else if (result.IsLockedOut) + { + _logger.LogWarning("User with ID {UserId} account locked out.", user.Id); + return RedirectToAction(nameof(Lockout)); + } + else + { + _logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id); + ModelState.AddModelError(string.Empty, "Invalid authenticator code."); + return View(); + } + } + + [HttpGet] + [AllowAnonymous] + public async Task LoginWithRecoveryCode(string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new ApplicationException($"Unable to load two-factor authentication user."); + } + + ViewData["ReturnUrl"] = returnUrl; + + return View(); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new ApplicationException($"Unable to load two-factor authentication user."); + } + + var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty); + + var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id); + return RedirectToLocal(returnUrl); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User with ID {UserId} account locked out.", user.Id); + return RedirectToAction(nameof(Lockout)); + } + else + { + _logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id); + ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); + return View(); + } + } + + [HttpGet] + [AllowAnonymous] + public IActionResult Lockout() + { + return View(); + } + [HttpGet] [AllowAnonymous] public IActionResult Register(string returnUrl = null) @@ -105,8 +215,6 @@ namespace Sample.Web.NetCore.Controllers return View(); } - // - // POST: /Account/Register [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] @@ -115,22 +223,18 @@ namespace Sample.Web.NetCore.Controllers ViewData["ReturnUrl"] = returnUrl; if (ModelState.IsValid) { - var user = new AppUser - { - UserName = model.Email, - Email = model.Email - }; + var user = new AppUser { UserName = model.Email, Email = model.Email }; var result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { - // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713 - // Send an email with this link - //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); - //var callbackUrl = Url.Action(nameof(ConfirmEmail), "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); - //await _emailSender.SendEmailAsync(model.Email, "Confirm your account", - // $"Please confirm your account by clicking this link: link"); + _logger.LogInformation("User created a new account with password."); + + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); + await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl); + await _signInManager.SignInAsync(user, isPersistent: false); - _logger.LogInformation(3, "User created a new account with password."); + _logger.LogInformation("User created a new account with password."); return RedirectToLocal(returnUrl); } AddErrors(result); @@ -140,40 +244,34 @@ namespace Sample.Web.NetCore.Controllers return View(model); } - // - // POST: /Account/Logout [HttpPost] [ValidateAntiForgeryToken] public async Task Logout() { await _signInManager.SignOutAsync(); - _logger.LogInformation(4, "User logged out."); + _logger.LogInformation("User logged out."); return RedirectToAction(nameof(HomeController.Index), "Home"); } - // - // POST: /Account/ExternalLogin [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public IActionResult ExternalLogin(string provider, string returnUrl = null) { // Request a redirect to the external login provider. - var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { ReturnUrl = returnUrl }); + var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl }); var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); return Challenge(properties, provider); } - // - // GET: /Account/ExternalLoginCallback [HttpGet] [AllowAnonymous] public async Task ExternalLoginCallback(string returnUrl = null, string remoteError = null) { if (remoteError != null) { - ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}"); - return View(nameof(Login)); + ErrorMessage = $"Error from external provider: {remoteError}"; + return RedirectToAction(nameof(Login)); } var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) @@ -182,19 +280,15 @@ namespace Sample.Web.NetCore.Controllers } // Sign in the user with this external login provider if the user already has a login. - var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false); + var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); if (result.Succeeded) { - _logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider); + _logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider); return RedirectToLocal(returnUrl); } - if (result.RequiresTwoFactor) - { - return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl }); - } if (result.IsLockedOut) { - return View("Lockout"); + return RedirectToAction(nameof(Lockout)); } else { @@ -202,16 +296,14 @@ namespace Sample.Web.NetCore.Controllers ViewData["ReturnUrl"] = returnUrl; ViewData["LoginProvider"] = info.LoginProvider; var email = info.Principal.FindFirstValue(ClaimTypes.Email); - return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email }); + return View("ExternalLogin", new ExternalLoginViewModel { Email = email }); } } - // - // POST: /Account/ExternalLoginConfirmation [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] - public async Task ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null) + public async Task ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null) { if (ModelState.IsValid) { @@ -219,7 +311,7 @@ namespace Sample.Web.NetCore.Controllers var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) { - return View("ExternalLoginFailure"); + throw new ApplicationException("Error loading external login information during confirmation."); } var user = new AppUser { UserName = model.Email, Email = model.Email }; var result = await _userManager.CreateAsync(user); @@ -229,7 +321,7 @@ namespace Sample.Web.NetCore.Controllers if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: false); - _logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider); + _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); return RedirectToLocal(returnUrl); } } @@ -237,29 +329,26 @@ namespace Sample.Web.NetCore.Controllers } ViewData["ReturnUrl"] = returnUrl; - return View(model); + return View(nameof(ExternalLogin), model); } - // GET: /Account/ConfirmEmail [HttpGet] [AllowAnonymous] public async Task ConfirmEmail(string userId, string code) { if (userId == null || code == null) { - return View("Error"); + return RedirectToAction(nameof(HomeController.Index), "Home"); } var user = await _userManager.FindByIdAsync(userId); if (user == null) { - return View("Error"); + throw new ApplicationException($"Unable to load user with ID '{userId}'."); } var result = await _userManager.ConfirmEmailAsync(user, code); return View(result.Succeeded ? "ConfirmEmail" : "Error"); } - // - // GET: /Account/ForgotPassword [HttpGet] [AllowAnonymous] public IActionResult ForgotPassword() @@ -267,8 +356,6 @@ namespace Sample.Web.NetCore.Controllers return View(); } - // - // POST: /Account/ForgotPassword [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] @@ -280,24 +367,22 @@ namespace Sample.Web.NetCore.Controllers if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) { // Don't reveal that the user does not exist or is not confirmed - return View("ForgotPasswordConfirmation"); + return RedirectToAction(nameof(ForgotPasswordConfirmation)); } - // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713 - // Send an email with this link - //var code = await _userManager.GeneratePasswordResetTokenAsync(user); - //var callbackUrl = Url.Action(nameof(ResetPassword), "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); - //await _emailSender.SendEmailAsync(model.Email, "Reset Password", - // $"Please reset your password by clicking here: link"); - //return View("ForgotPasswordConfirmation"); + // For more information on how to enable account confirmation and password reset please + // visit https://go.microsoft.com/fwlink/?LinkID=532713 + var code = await _userManager.GeneratePasswordResetTokenAsync(user); + var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme); + await _emailSender.SendEmailAsync(model.Email, "Reset Password", + $"Please reset your password by clicking here: link"); + return RedirectToAction(nameof(ForgotPasswordConfirmation)); } // If we got this far, something failed, redisplay form return View(model); } - // - // GET: /Account/ForgotPasswordConfirmation [HttpGet] [AllowAnonymous] public IActionResult ForgotPasswordConfirmation() @@ -305,17 +390,18 @@ namespace Sample.Web.NetCore.Controllers return View(); } - // - // GET: /Account/ResetPassword [HttpGet] [AllowAnonymous] public IActionResult ResetPassword(string code = null) { - return code == null ? View("Error") : View(); + if (code == null) + { + throw new ApplicationException("A code must be supplied for password reset."); + } + var model = new ResetPasswordViewModel { Code = code }; + return View(model); } - // - // POST: /Account/ResetPassword [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] @@ -329,19 +415,17 @@ namespace Sample.Web.NetCore.Controllers if (user == null) { // Don't reveal that the user does not exist - return RedirectToAction(nameof(AccountController.ResetPasswordConfirmation), "Account"); + return RedirectToAction(nameof(ResetPasswordConfirmation)); } var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password); if (result.Succeeded) { - return RedirectToAction(nameof(AccountController.ResetPasswordConfirmation), "Account"); + return RedirectToAction(nameof(ResetPasswordConfirmation)); } AddErrors(result); return View(); } - // - // GET: /Account/ResetPasswordConfirmation [HttpGet] [AllowAnonymous] public IActionResult ResetPasswordConfirmation() @@ -349,109 +433,7 @@ namespace Sample.Web.NetCore.Controllers return View(); } - // - // GET: /Account/SendCode - [HttpGet] - [AllowAnonymous] - public async Task SendCode(string returnUrl = null, bool rememberMe = false) - { - var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); - if (user == null) - { - return View("Error"); - } - var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user); - var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList(); - return View(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl, RememberMe = rememberMe }); - } - // - // POST: /Account/SendCode - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public async Task SendCode(SendCodeViewModel model) - { - if (!ModelState.IsValid) - { - return View(); - } - - var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); - if (user == null) - { - return View("Error"); - } - - // Generate the token and send it - var code = await _userManager.GenerateTwoFactorTokenAsync(user, model.SelectedProvider); - if (string.IsNullOrWhiteSpace(code)) - { - return View("Error"); - } - - var message = "Your security code is: " + code; - if (model.SelectedProvider == "Email") - { - await _emailSender.SendEmailAsync(await _userManager.GetEmailAsync(user), "Security Code", message); - } - else if (model.SelectedProvider == "Phone") - { - await _smsSender.SendSmsAsync(await _userManager.GetPhoneNumberAsync(user), message); - } - - return RedirectToAction(nameof(VerifyCode), new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe }); - } - - // - // GET: /Account/VerifyCode - [HttpGet] - [AllowAnonymous] - public async Task VerifyCode(string provider, bool rememberMe, string returnUrl = null) - { - // Require that the user has already logged in via username/password or external login - var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); - if (user == null) - { - return View("Error"); - } - return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl, RememberMe = rememberMe }); - } - - // - // POST: /Account/VerifyCode - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public async Task VerifyCode(VerifyCodeViewModel model) - { - if (!ModelState.IsValid) - { - return View(model); - } - - // The following code protects for brute force attacks against the two factor codes. - // If a user enters incorrect codes for a specified amount of time then the user account - // will be locked out for a specified amount of time. - var result = await _signInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberBrowser); - if (result.Succeeded) - { - return RedirectToLocal(model.ReturnUrl); - } - if (result.IsLockedOut) - { - _logger.LogWarning(7, "User account locked out."); - return View("Lockout"); - } - else - { - ModelState.AddModelError(string.Empty, "Invalid code."); - return View(model); - } - } - - // - // GET: /Account/AccessDenied [HttpGet] public IActionResult AccessDenied() { diff --git a/Sample/Controllers/HomeController.cs b/Sample/Controllers/HomeController.cs index 64a5ad1..c71697e 100644 --- a/Sample/Controllers/HomeController.cs +++ b/Sample/Controllers/HomeController.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Sample.Models; using Raven.Client; -using Sample.Web.NetCore.Models; -namespace Sample.Web.NetCore.Controllers +namespace Sample.Controllers { public class HomeController : RavenController { diff --git a/Sample/Controllers/ManageController.cs b/Sample/Controllers/ManageController.cs index 1095588..39c5b9f 100644 --- a/Sample/Controllers/ManageController.cs +++ b/Sample/Controllers/ManageController.cs @@ -1,223 +1,153 @@ using System; using System.Collections.Generic; 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.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Sample.Web.NetCore.Models; -using Sample.Web.NetCore.Models.ManageViewModels; -using Sample.Web.NetCore.Services; +using Sample.Models; +using Sample.Models.ManageViewModels; +using Sample.Services; -namespace Sample.Web.NetCore.Controllers +namespace Sample.Controllers { [Authorize] + [Route("[controller]/[action]")] public class ManageController : Controller { private readonly UserManager _userManager; private readonly SignInManager _signInManager; - private readonly string _externalCookieScheme; private readonly IEmailSender _emailSender; - private readonly ISmsSender _smsSender; private readonly ILogger _logger; + private readonly UrlEncoder _urlEncoder; + + private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; public ManageController( UserManager userManager, SignInManager signInManager, - IOptions identityCookieOptions, IEmailSender emailSender, - ISmsSender smsSender, - ILoggerFactory loggerFactory) + ILogger logger, + UrlEncoder urlEncoder) { _userManager = userManager; _signInManager = signInManager; - _externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme; _emailSender = emailSender; - _smsSender = smsSender; - _logger = loggerFactory.CreateLogger(); + _logger = logger; + _urlEncoder = urlEncoder; } - // - // GET: /Manage/Index - [HttpGet] - public async Task Index(ManageMessageId? message = null) - { - ViewData["StatusMessage"] = - message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." - : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set." - : message == ManageMessageId.SetTwoFactorSuccess ? "Your two-factor authentication provider has been set." - : message == ManageMessageId.Error ? "An error has occurred." - : message == ManageMessageId.AddPhoneSuccess ? "Your phone number was added." - : message == ManageMessageId.RemovePhoneSuccess ? "Your phone number was removed." - : ""; + [TempData] + public string StatusMessage { get; set; } - var user = await GetCurrentUserAsync(); + [HttpGet] + public async Task Index() + { + var user = await _userManager.GetUserAsync(User); if (user == null) { - return View("Error"); + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } + var model = new IndexViewModel { - HasPassword = await _userManager.HasPasswordAsync(user), - PhoneNumber = await _userManager.GetPhoneNumberAsync(user), - TwoFactor = await _userManager.GetTwoFactorEnabledAsync(user), - Logins = await _userManager.GetLoginsAsync(user), - BrowserRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user) + Username = user.UserName, + Email = user.Email, + PhoneNumber = user.PhoneNumber, + IsEmailConfirmed = user.IsEmailConfirmed, + StatusMessage = StatusMessage }; + return View(model); } - // - // POST: /Manage/RemoveLogin [HttpPost] [ValidateAntiForgeryToken] - public async Task RemoveLogin(RemoveLoginViewModel account) - { - ManageMessageId? message = ManageMessageId.Error; - var user = await GetCurrentUserAsync(); - if (user != null) - { - var result = await _userManager.RemoveLoginAsync(user, account.LoginProvider, account.ProviderKey); - if (result.Succeeded) - { - await _signInManager.SignInAsync(user, isPersistent: false); - message = ManageMessageId.RemoveLoginSuccess; - } - } - return RedirectToAction(nameof(ManageLogins), new { Message = message }); - } - - // - // GET: /Manage/AddPhoneNumber - public IActionResult AddPhoneNumber() - { - return View(); - } - - // - // POST: /Manage/AddPhoneNumber - [HttpPost] - [ValidateAntiForgeryToken] - public async Task AddPhoneNumber(AddPhoneNumberViewModel model) + public async Task Index(IndexViewModel model) { if (!ModelState.IsValid) { return View(model); } - // Generate the token and send it - var user = await GetCurrentUserAsync(); + + var user = await _userManager.GetUserAsync(User); if (user == null) { - return View("Error"); + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - var code = await _userManager.GenerateChangePhoneNumberTokenAsync(user, model.PhoneNumber); - await _smsSender.SendSmsAsync(model.PhoneNumber, "Your security code is: " + code); - return RedirectToAction(nameof(VerifyPhoneNumber), new { PhoneNumber = model.PhoneNumber }); + + var email = user.Email; + if (model.Email != email) + { + var setEmailResult = await _userManager.SetEmailAsync(user, model.Email); + if (!setEmailResult.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'."); + } + } + + var phoneNumber = user.PhoneNumber; + if (model.PhoneNumber != phoneNumber) + { + var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, model.PhoneNumber); + if (!setPhoneResult.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred setting phone number for user with ID '{user.Id}'."); + } + } + + StatusMessage = "Your profile has been updated"; + return RedirectToAction(nameof(Index)); } - // - // POST: /Manage/EnableTwoFactorAuthentication [HttpPost] [ValidateAntiForgeryToken] - public async Task EnableTwoFactorAuthentication() - { - var user = await GetCurrentUserAsync(); - if (user != null) - { - await _userManager.SetTwoFactorEnabledAsync(user, true); - await _signInManager.SignInAsync(user, isPersistent: false); - _logger.LogInformation(1, "User enabled two-factor authentication."); - } - return RedirectToAction(nameof(Index), "Manage"); - } - - // - // POST: /Manage/DisableTwoFactorAuthentication - [HttpPost] - [ValidateAntiForgeryToken] - public async Task DisableTwoFactorAuthentication() - { - var user = await GetCurrentUserAsync(); - if (user != null) - { - await _userManager.SetTwoFactorEnabledAsync(user, false); - await _signInManager.SignInAsync(user, isPersistent: false); - _logger.LogInformation(2, "User disabled two-factor authentication."); - } - return RedirectToAction(nameof(Index), "Manage"); - } - - // - // GET: /Manage/VerifyPhoneNumber - [HttpGet] - public async Task VerifyPhoneNumber(string phoneNumber) - { - var user = await GetCurrentUserAsync(); - if (user == null) - { - return View("Error"); - } - var code = await _userManager.GenerateChangePhoneNumberTokenAsync(user, phoneNumber); - // Send an SMS to verify the phone number - return phoneNumber == null ? View("Error") : View(new VerifyPhoneNumberViewModel { PhoneNumber = phoneNumber }); - } - - // - // POST: /Manage/VerifyPhoneNumber - [HttpPost] - [ValidateAntiForgeryToken] - public async Task VerifyPhoneNumber(VerifyPhoneNumberViewModel model) + public async Task SendVerificationEmail(IndexViewModel model) { if (!ModelState.IsValid) { return View(model); } - var user = await GetCurrentUserAsync(); - if (user != null) + + var user = await _userManager.GetUserAsync(User); + if (user == null) { - var result = await _userManager.ChangePhoneNumberAsync(user, model.PhoneNumber, model.Code); - if (result.Succeeded) - { - await _signInManager.SignInAsync(user, isPersistent: false); - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.AddPhoneSuccess }); - } + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - // If we got this far, something failed, redisplay the form - ModelState.AddModelError(string.Empty, "Failed to verify phone number"); + + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); + var email = user.Email; + await _emailSender.SendEmailConfirmationAsync(email, callbackUrl); + + StatusMessage = "Verification email sent. Please check your email."; + return RedirectToAction(nameof(Index)); + } + + [HttpGet] + public async Task ChangePassword() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + if (!hasPassword) + { + return RedirectToAction(nameof(SetPassword)); + } + + var model = new ChangePasswordViewModel { StatusMessage = StatusMessage }; return View(model); } - // - // POST: /Manage/RemovePhoneNumber - [HttpPost] - [ValidateAntiForgeryToken] - public async Task RemovePhoneNumber() - { - var user = await GetCurrentUserAsync(); - if (user != null) - { - var result = await _userManager.SetPhoneNumberAsync(user, null); - if (result.Succeeded) - { - await _signInManager.SignInAsync(user, isPersistent: false); - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.RemovePhoneSuccess }); - } - } - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); - } - - // - // GET: /Manage/ChangePassword - [HttpGet] - public IActionResult ChangePassword() - { - return View(); - } - - // - // POST: /Manage/ChangePassword [HttpPost] [ValidateAntiForgeryToken] public async Task ChangePassword(ChangePasswordViewModel model) @@ -226,32 +156,47 @@ namespace Sample.Web.NetCore.Controllers { return View(model); } - var user = await GetCurrentUserAsync(); - if (user != null) + + var user = await _userManager.GetUserAsync(User); + if (user == null) { - var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); - if (result.Succeeded) - { - await _signInManager.SignInAsync(user, isPersistent: false); - _logger.LogInformation(3, "User changed their password successfully."); - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.ChangePasswordSuccess }); - } - AddErrors(result); + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var changePasswordResult = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); + if (!changePasswordResult.Succeeded) + { + AddErrors(changePasswordResult); return View(model); } - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); + + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation("User changed their password successfully."); + StatusMessage = "Your password has been changed."; + + return RedirectToAction(nameof(ChangePassword)); } - // - // GET: /Manage/SetPassword [HttpGet] - public IActionResult SetPassword() + public async Task SetPassword() { - return View(); + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + + if (hasPassword) + { + return RedirectToAction(nameof(ChangePassword)); + } + + var model = new SetPasswordViewModel { StatusMessage = StatusMessage }; + return View(model); } - // - // POST: /Manage/SetPassword [HttpPost] [ValidateAntiForgeryToken] public async Task SetPassword(SetPasswordViewModel model) @@ -261,84 +206,262 @@ namespace Sample.Web.NetCore.Controllers return View(model); } - var user = await GetCurrentUserAsync(); - if (user != null) - { - var result = await _userManager.AddPasswordAsync(user, model.NewPassword); - if (result.Succeeded) - { - await _signInManager.SignInAsync(user, isPersistent: false); - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetPasswordSuccess }); - } - AddErrors(result); - return View(model); - } - return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); - } - - //GET: /Manage/ManageLogins - [HttpGet] - public async Task ManageLogins(ManageMessageId? message = null) - { - ViewData["StatusMessage"] = - message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." - : message == ManageMessageId.AddLoginSuccess ? "The external login was added." - : message == ManageMessageId.Error ? "An error has occurred." - : ""; - var user = await GetCurrentUserAsync(); + var user = await _userManager.GetUserAsync(User); if (user == null) { - return View("Error"); + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - var userLogins = await _userManager.GetLoginsAsync(user); - var otherLogins = _signInManager.GetExternalAuthenticationSchemes().Where(auth => userLogins.All(ul => auth.AuthenticationScheme != ul.LoginProvider)).ToList(); - ViewData["ShowRemoveButton"] = user.PasswordHash != null || userLogins.Count > 1; - return View(new ManageLoginsViewModel + + var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword); + if (!addPasswordResult.Succeeded) { - CurrentLogins = userLogins, - OtherLogins = otherLogins - }); + AddErrors(addPasswordResult); + return View(model); + } + + await _signInManager.SignInAsync(user, isPersistent: false); + StatusMessage = "Your password has been set."; + + return RedirectToAction(nameof(SetPassword)); + } + + [HttpGet] + public async Task ExternalLogins() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var model = new ExternalLoginsViewModel { CurrentLogins = await _userManager.GetLoginsAsync(user) }; + model.OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) + .Where(auth => model.CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) + .ToList(); + model.ShowRemoveButton = await _userManager.HasPasswordAsync(user) || model.CurrentLogins.Count > 1; + model.StatusMessage = StatusMessage; + + return View(model); } - // - // POST: /Manage/LinkLogin [HttpPost] [ValidateAntiForgeryToken] public async Task LinkLogin(string provider) { // Clear the existing external cookie to ensure a clean login process - await HttpContext.Authentication.SignOutAsync(_externalCookieScheme); + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); // Request a redirect to the external login provider to link a login for the current user - var redirectUrl = Url.Action(nameof(LinkLoginCallback), "Manage"); + var redirectUrl = Url.Action(nameof(LinkLoginCallback)); var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); - return Challenge(properties, provider); + return new ChallengeResult(provider, properties); } - // - // GET: /Manage/LinkLoginCallback [HttpGet] - public async Task LinkLoginCallback() + public async Task LinkLoginCallback() { - var user = await GetCurrentUserAsync(); + var user = await _userManager.GetUserAsync(User); if (user == null) { - return View("Error"); + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - var info = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user)); + + var info = await _signInManager.GetExternalLoginInfoAsync(user.Id); if (info == null) { - return RedirectToAction(nameof(ManageLogins), new { Message = ManageMessageId.Error }); + throw new ApplicationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'."); } + var result = await _userManager.AddLoginAsync(user, info); - var message = ManageMessageId.Error; - if (result.Succeeded) + if (!result.Succeeded) { - message = ManageMessageId.AddLoginSuccess; - // Clear the existing external cookie to ensure a clean login process - await HttpContext.Authentication.SignOutAsync(_externalCookieScheme); + throw new ApplicationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'."); } - return RedirectToAction(nameof(ManageLogins), new { Message = message }); + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + StatusMessage = "The external login was added."; + return RedirectToAction(nameof(ExternalLogins)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task RemoveLogin(RemoveLoginViewModel model) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey); + if (!result.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred removing external login for user with ID '{user.Id}'."); + } + + await _signInManager.SignInAsync(user, isPersistent: false); + StatusMessage = "The external login was removed."; + return RedirectToAction(nameof(ExternalLogins)); + } + + [HttpGet] + public async Task TwoFactorAuthentication() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var model = new TwoFactorAuthenticationViewModel + { + HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null, + Is2faEnabled = user.TwoFactorAuthEnabled, + RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user), + }; + + return View(model); + } + + [HttpGet] + public async Task Disable2faWarning() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!user.TwoFactorAuthEnabled) + { + throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'."); + } + + return View(nameof(Disable2fa)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Disable2fa() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); + if (!disable2faResult.Succeeded) + { + throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'."); + } + + _logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id); + return RedirectToAction(nameof(TwoFactorAuthentication)); + } + + [HttpGet] + public async Task EnableAuthenticator() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + } + + var model = new EnableAuthenticatorViewModel + { + SharedKey = FormatKey(unformattedKey), + AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey) + }; + + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task EnableAuthenticator(EnableAuthenticatorViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + // Strip spaces and hypens + var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty); + + var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( + user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + ModelState.AddModelError("model.Code", "Verification code is invalid."); + return View(model); + } + + await _userManager.SetTwoFactorEnabledAsync(user, true); + _logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id); + return RedirectToAction(nameof(GenerateRecoveryCodes)); + } + + [HttpGet] + public IActionResult ResetAuthenticatorWarning() + { + return View(nameof(ResetAuthenticator)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ResetAuthenticator() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _userManager.ResetAuthenticatorKeyAsync(user); + _logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id); + + return RedirectToAction(nameof(EnableAuthenticator)); + } + + [HttpGet] + public async Task GenerateRecoveryCodes() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!user.TwoFactorAuthEnabled) + { + throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); + } + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() }; + + _logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id); + + return View(model); } #region Helpers @@ -351,21 +474,30 @@ namespace Sample.Web.NetCore.Controllers } } - public enum ManageMessageId + private string FormatKey(string unformattedKey) { - AddPhoneSuccess, - AddLoginSuccess, - ChangePasswordSuccess, - SetTwoFactorSuccess, - SetPasswordSuccess, - RemoveLoginSuccess, - RemovePhoneSuccess, - Error + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); } - private Task GetCurrentUserAsync() + private string GenerateQrCodeUri(string email, string unformattedKey) { - return _userManager.GetUserAsync(HttpContext.User); + return string.Format( + AuthenicatorUriFormat, + _urlEncoder.Encode("Sample"), + _urlEncoder.Encode(email), + unformattedKey); } #endregion diff --git a/Sample/Controllers/RavenController.cs b/Sample/Controllers/RavenController.cs index d5babfa..5b95909 100644 --- a/Sample/Controllers/RavenController.cs +++ b/Sample/Controllers/RavenController.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Filters; -namespace Sample.Web.NetCore.Controllers +namespace Sample.Controllers { /// /// Controller that saves changes on the RavenDB document session. @@ -18,7 +18,7 @@ namespace Sample.Web.NetCore.Controllers DbSession = dbSession ?? throw new ArgumentNullException(nameof(dbSession)); // RavenDB best practice: during save, wait for the indexes to update. - // This way, Post-Redirect-Get scenarios are covered, not affected by stale indexes. + // This way, Post-Redirect-Get scenarios won't be affected by stale indexes. // For more info, see https://ravendb.net/docs/article-page/3.5/Csharp/client-api/session/saving-changes DbSession.Advanced.WaitForIndexesAfterSaveChanges(timeout: TimeSpan.FromSeconds(30), throwOnTimeout: false); } diff --git a/Sample/Extensions/EmailSenderExtensions.cs b/Sample/Extensions/EmailSenderExtensions.cs new file mode 100644 index 0000000..5f1380a --- /dev/null +++ b/Sample/Extensions/EmailSenderExtensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Sample.Services; + +namespace Sample.Services +{ + public static class EmailSenderExtensions + { + public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link) + { + return emailSender.SendEmailAsync(email, "Confirm your email", + $"Please confirm your account by clicking this link: link"); + } + } +} diff --git a/Sample/Extensions/UrlHelperExtensions.cs b/Sample/Extensions/UrlHelperExtensions.cs new file mode 100644 index 0000000..57e7c8b --- /dev/null +++ b/Sample/Extensions/UrlHelperExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Sample.Controllers; + +namespace Microsoft.AspNetCore.Mvc +{ + public static class UrlHelperExtensions + { + public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme) + { + return urlHelper.Action( + action: nameof(AccountController.ConfirmEmail), + controller: "Account", + values: new { userId, code }, + protocol: scheme); + } + + public static string ResetPasswordCallbackLink(this IUrlHelper urlHelper, string userId, string code, string scheme) + { + return urlHelper.Action( + action: nameof(AccountController.ResetPassword), + controller: "Account", + values: new { userId, code }, + protocol: scheme); + } + } +} diff --git a/Sample/Models/AccountViewModels/ExternalLoginConfirmationViewModel.cs b/Sample/Models/AccountViewModels/ExternalLoginViewModel.cs similarity index 69% rename from Sample/Models/AccountViewModels/ExternalLoginConfirmationViewModel.cs rename to Sample/Models/AccountViewModels/ExternalLoginViewModel.cs index 576f499..8e9159f 100644 --- a/Sample/Models/AccountViewModels/ExternalLoginConfirmationViewModel.cs +++ b/Sample/Models/AccountViewModels/ExternalLoginViewModel.cs @@ -4,9 +4,9 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -namespace Sample.Web.NetCore.Models.AccountViewModels +namespace Sample.Models.AccountViewModels { - public class ExternalLoginConfirmationViewModel + public class ExternalLoginViewModel { [Required] [EmailAddress] diff --git a/Sample/Models/AccountViewModels/ForgotPasswordViewModel.cs b/Sample/Models/AccountViewModels/ForgotPasswordViewModel.cs index 206ea35..daaa4aa 100644 --- a/Sample/Models/AccountViewModels/ForgotPasswordViewModel.cs +++ b/Sample/Models/AccountViewModels/ForgotPasswordViewModel.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -namespace Sample.Web.NetCore.Models.AccountViewModels +namespace Sample.Models.AccountViewModels { public class ForgotPasswordViewModel { diff --git a/Sample/Models/AccountViewModels/LoginViewModel.cs b/Sample/Models/AccountViewModels/LoginViewModel.cs index 9f774d7..07c8e0c 100644 --- a/Sample/Models/AccountViewModels/LoginViewModel.cs +++ b/Sample/Models/AccountViewModels/LoginViewModel.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -namespace Sample.Web.NetCore.Models.AccountViewModels +namespace Sample.Models.AccountViewModels { public class LoginViewModel { diff --git a/Sample/Models/AccountViewModels/LoginWith2faViewModel.cs b/Sample/Models/AccountViewModels/LoginWith2faViewModel.cs new file mode 100644 index 0000000..4f34f0c --- /dev/null +++ b/Sample/Models/AccountViewModels/LoginWith2faViewModel.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Sample.Models.AccountViewModels +{ + public class LoginWith2faViewModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Authenticator code")] + public string TwoFactorCode { get; set; } + + [Display(Name = "Remember this machine")] + public bool RememberMachine { get; set; } + + public bool RememberMe { get; set; } + } +} diff --git a/Sample/Models/AccountViewModels/LoginWithRecoveryCodeViewModel.cs b/Sample/Models/AccountViewModels/LoginWithRecoveryCodeViewModel.cs new file mode 100644 index 0000000..faa2a83 --- /dev/null +++ b/Sample/Models/AccountViewModels/LoginWithRecoveryCodeViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Sample.Models.AccountViewModels +{ + public class LoginWithRecoveryCodeViewModel + { + [Required] + [DataType(DataType.Text)] + [Display(Name = "Recovery Code")] + public string RecoveryCode { get; set; } + } +} diff --git a/Sample/Models/AccountViewModels/RegisterViewModel.cs b/Sample/Models/AccountViewModels/RegisterViewModel.cs index 0c4f55c..86c34b0 100644 --- a/Sample/Models/AccountViewModels/RegisterViewModel.cs +++ b/Sample/Models/AccountViewModels/RegisterViewModel.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -namespace Sample.Web.NetCore.Models.AccountViewModels +namespace Sample.Models.AccountViewModels { public class RegisterViewModel { diff --git a/Sample/Models/AccountViewModels/ResetPasswordViewModel.cs b/Sample/Models/AccountViewModels/ResetPasswordViewModel.cs index d2ff42d..d9ac584 100644 --- a/Sample/Models/AccountViewModels/ResetPasswordViewModel.cs +++ b/Sample/Models/AccountViewModels/ResetPasswordViewModel.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -namespace Sample.Web.NetCore.Models.AccountViewModels +namespace Sample.Models.AccountViewModels { public class ResetPasswordViewModel { diff --git a/Sample/Models/AccountViewModels/SendCodeViewModel.cs b/Sample/Models/AccountViewModels/SendCodeViewModel.cs deleted file mode 100644 index 39c8272..0000000 --- a/Sample/Models/AccountViewModels/SendCodeViewModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Rendering; - -namespace Sample.Web.NetCore.Models.AccountViewModels -{ - public class SendCodeViewModel - { - public string SelectedProvider { get; set; } - - public ICollection Providers { get; set; } - - public string ReturnUrl { get; set; } - - public bool RememberMe { get; set; } - } -} diff --git a/Sample/Models/AccountViewModels/VerifyCodeViewModel.cs b/Sample/Models/AccountViewModels/VerifyCodeViewModel.cs deleted file mode 100644 index 10756d2..0000000 --- a/Sample/Models/AccountViewModels/VerifyCodeViewModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; - -namespace Sample.Web.NetCore.Models.AccountViewModels -{ - public class VerifyCodeViewModel - { - [Required] - public string Provider { get; set; } - - [Required] - public string Code { get; set; } - - public string ReturnUrl { get; set; } - - [Display(Name = "Remember this browser?")] - public bool RememberBrowser { get; set; } - - [Display(Name = "Remember me?")] - public bool RememberMe { get; set; } - } -} diff --git a/Sample/Models/AppUser.cs b/Sample/Models/AppUser.cs index c68a0e0..0824bad 100644 --- a/Sample/Models/AppUser.cs +++ b/Sample/Models/AppUser.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Threading.Tasks; using RavenDB.Identity; -namespace Sample.Web.NetCore.Models +namespace Sample.Models { // Add profile data for application users by adding properties to the AppUser class public class AppUser : IdentityUser diff --git a/Sample/Models/ErrorViewModel.cs b/Sample/Models/ErrorViewModel.cs new file mode 100644 index 0000000..68f449e --- /dev/null +++ b/Sample/Models/ErrorViewModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace Sample.Models +{ + public class ErrorViewModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + } +} \ No newline at end of file diff --git a/Sample/Models/ManageViewModels/AddPhoneNumberViewModel.cs b/Sample/Models/ManageViewModels/AddPhoneNumberViewModel.cs deleted file mode 100644 index e4dbce8..0000000 --- a/Sample/Models/ManageViewModels/AddPhoneNumberViewModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; - -namespace Sample.Web.NetCore.Models.ManageViewModels -{ - public class AddPhoneNumberViewModel - { - [Required] - [Phone] - [Display(Name = "Phone number")] - public string PhoneNumber { get; set; } - } -} diff --git a/Sample/Models/ManageViewModels/ChangePasswordViewModel.cs b/Sample/Models/ManageViewModels/ChangePasswordViewModel.cs index 4579a49..f914392 100644 --- a/Sample/Models/ManageViewModels/ChangePasswordViewModel.cs +++ b/Sample/Models/ManageViewModels/ChangePasswordViewModel.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -namespace Sample.Web.NetCore.Models.ManageViewModels +namespace Sample.Models.ManageViewModels { public class ChangePasswordViewModel { @@ -23,5 +23,7 @@ namespace Sample.Web.NetCore.Models.ManageViewModels [Display(Name = "Confirm new password")] [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] public string ConfirmPassword { get; set; } + + public string StatusMessage { get; set; } } } diff --git a/Sample/Models/ManageViewModels/ConfigureTwoFactorViewModel.cs b/Sample/Models/ManageViewModels/ConfigureTwoFactorViewModel.cs deleted file mode 100644 index b82e759..0000000 --- a/Sample/Models/ManageViewModels/ConfigureTwoFactorViewModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Rendering; - -namespace Sample.Web.NetCore.Models.ManageViewModels -{ - public class ConfigureTwoFactorViewModel - { - public string SelectedProvider { get; set; } - - public ICollection Providers { get; set; } - } -} diff --git a/Sample/Models/ManageViewModels/EnableAuthenticatorViewModel.cs b/Sample/Models/ManageViewModels/EnableAuthenticatorViewModel.cs new file mode 100644 index 0000000..4c39fed --- /dev/null +++ b/Sample/Models/ManageViewModels/EnableAuthenticatorViewModel.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Sample.Models.ManageViewModels +{ + public class EnableAuthenticatorViewModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Verification Code")] + public string Code { get; set; } + + [ReadOnly(true)] + public string SharedKey { get; set; } + + public string AuthenticatorUri { get; set; } + } +} diff --git a/Sample/Models/ManageViewModels/ExternalLoginsViewModel.cs b/Sample/Models/ManageViewModels/ExternalLoginsViewModel.cs new file mode 100644 index 0000000..0737ae3 --- /dev/null +++ b/Sample/Models/ManageViewModels/ExternalLoginsViewModel.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; + +namespace Sample.Models.ManageViewModels +{ + public class ExternalLoginsViewModel + { + public IList CurrentLogins { get; set; } + + public IList OtherLogins { get; set; } + + public bool ShowRemoveButton { get; set; } + + public string StatusMessage { get; set; } + } +} diff --git a/Sample/Models/ManageViewModels/FactorViewModel.cs b/Sample/Models/ManageViewModels/FactorViewModel.cs deleted file mode 100644 index a0626fe..0000000 --- a/Sample/Models/ManageViewModels/FactorViewModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Sample.Web.NetCore.Models.ManageViewModels -{ - public class FactorViewModel - { - public string Purpose { get; set; } - } -} diff --git a/Sample/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs b/Sample/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs new file mode 100644 index 0000000..8d8909e --- /dev/null +++ b/Sample/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Sample.Models.ManageViewModels +{ + public class GenerateRecoveryCodesViewModel + { + public string[] RecoveryCodes { get; set; } + } +} diff --git a/Sample/Models/ManageViewModels/IndexViewModel.cs b/Sample/Models/ManageViewModels/IndexViewModel.cs index 146d814..56ddebf 100644 --- a/Sample/Models/ManageViewModels/IndexViewModel.cs +++ b/Sample/Models/ManageViewModels/IndexViewModel.cs @@ -1,21 +1,25 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -namespace Sample.Web.NetCore.Models.ManageViewModels +namespace Sample.Models.ManageViewModels { public class IndexViewModel { - public bool HasPassword { get; set; } + public string Username { get; set; } - public IList Logins { get; set; } + public bool IsEmailConfirmed { get; set; } + [Required] + [EmailAddress] + public string Email { get; set; } + + [Phone] + [Display(Name = "Phone number")] public string PhoneNumber { get; set; } - public bool TwoFactor { get; set; } - - public bool BrowserRemembered { get; set; } + public string StatusMessage { get; set; } } } diff --git a/Sample/Models/ManageViewModels/ManageLoginsViewModel.cs b/Sample/Models/ManageViewModels/ManageLoginsViewModel.cs deleted file mode 100644 index 05bdac5..0000000 --- a/Sample/Models/ManageViewModels/ManageLoginsViewModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Identity; - -namespace Sample.Web.NetCore.Models.ManageViewModels -{ - public class ManageLoginsViewModel - { - public IList CurrentLogins { get; set; } - - public IList OtherLogins { get; set; } - } -} diff --git a/Sample/Models/ManageViewModels/RemoveLoginViewModel.cs b/Sample/Models/ManageViewModels/RemoveLoginViewModel.cs index 1085d3c..e84c952 100644 --- a/Sample/Models/ManageViewModels/RemoveLoginViewModel.cs +++ b/Sample/Models/ManageViewModels/RemoveLoginViewModel.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -namespace Sample.Web.NetCore.Models.ManageViewModels +namespace Sample.Models.ManageViewModels { public class RemoveLoginViewModel { diff --git a/Sample/Models/ManageViewModels/SetPasswordViewModel.cs b/Sample/Models/ManageViewModels/SetPasswordViewModel.cs index a703230..cd5fa6a 100644 --- a/Sample/Models/ManageViewModels/SetPasswordViewModel.cs +++ b/Sample/Models/ManageViewModels/SetPasswordViewModel.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -namespace Sample.Web.NetCore.Models.ManageViewModels +namespace Sample.Models.ManageViewModels { public class SetPasswordViewModel { @@ -18,5 +18,7 @@ namespace Sample.Web.NetCore.Models.ManageViewModels [Display(Name = "Confirm new password")] [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] public string ConfirmPassword { get; set; } + + public string StatusMessage { get; set; } } } diff --git a/Sample/Models/ManageViewModels/TwoFactorAuthenticationViewModel.cs b/Sample/Models/ManageViewModels/TwoFactorAuthenticationViewModel.cs new file mode 100644 index 0000000..18e3a1b --- /dev/null +++ b/Sample/Models/ManageViewModels/TwoFactorAuthenticationViewModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Sample.Models.ManageViewModels +{ + public class TwoFactorAuthenticationViewModel + { + public bool HasAuthenticator { get; set; } + + public int RecoveryCodesLeft { get; set; } + + public bool Is2faEnabled { get; set; } + } +} diff --git a/Sample/Models/ManageViewModels/VerifyPhoneNumberViewModel.cs b/Sample/Models/ManageViewModels/VerifyPhoneNumberViewModel.cs deleted file mode 100644 index 7d76bb8..0000000 --- a/Sample/Models/ManageViewModels/VerifyPhoneNumberViewModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; - -namespace Sample.Web.NetCore.Models.ManageViewModels -{ - public class VerifyPhoneNumberViewModel - { - [Required] - public string Code { get; set; } - - [Required] - [Phone] - [Display(Name = "Phone number")] - public string PhoneNumber { get; set; } - } -} diff --git a/Sample/Program.cs b/Sample/Program.cs index dd474d6..caa99bf 100644 --- a/Sample/Program.cs +++ b/Sample/Program.cs @@ -3,23 +3,23 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; -namespace Sample.Web.NetCore +namespace Sample { public class Program { public static void Main(string[] args) { - var host = new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseStartup() - .UseApplicationInsights() - .Build(); - - host.Run(); + BuildWebHost(args).Run(); } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); } } diff --git a/Sample/Sample.csproj b/Sample/Sample.csproj index d06a1e9..1c7ffbb 100644 --- a/Sample/Sample.csproj +++ b/Sample/Sample.csproj @@ -1,36 +1,21 @@ - netcoreapp1.1 - $(PackageTargetFallback);portable-net45+win8+wp8+wpa81; - aspnet-Sample.Web.NetCore-56FAE12E-CFF5-44D1-AE0F-D5E908F78EF0 + netcoreapp2.0 + aspnet-Sample-ABF406ED-6407-4847-8914-417141BB844E - - - - - - - - - + + + - - - - - - - - - - - + + + diff --git a/Sample/Services/EmailSender.cs b/Sample/Services/EmailSender.cs new file mode 100644 index 0000000..756f8ab --- /dev/null +++ b/Sample/Services/EmailSender.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Sample.Services +{ + // This class is used by the application to send email for account confirmation and password reset. + // For more details see https://go.microsoft.com/fwlink/?LinkID=532713 + public class EmailSender : IEmailSender + { + public Task SendEmailAsync(string email, string subject, string message) + { + return Task.CompletedTask; + } + } +} diff --git a/Sample/Services/IEmailSender.cs b/Sample/Services/IEmailSender.cs index 2b05bf3..e9228e0 100644 --- a/Sample/Services/IEmailSender.cs +++ b/Sample/Services/IEmailSender.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -namespace Sample.Web.NetCore.Services +namespace Sample.Services { public interface IEmailSender { diff --git a/Sample/Services/ISmsSender.cs b/Sample/Services/ISmsSender.cs deleted file mode 100644 index ce7805f..0000000 --- a/Sample/Services/ISmsSender.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Sample.Web.NetCore.Services -{ - public interface ISmsSender - { - Task SendSmsAsync(string number, string message); - } -} diff --git a/Sample/Services/MessageServices.cs b/Sample/Services/MessageServices.cs deleted file mode 100644 index 8189503..0000000 --- a/Sample/Services/MessageServices.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Sample.Web.NetCore.Services -{ - // This class is used by the application to send Email and SMS - // when you turn on two-factor authentication in ASP.NET Identity. - // For more details see this link https://go.microsoft.com/fwlink/?LinkID=532713 - public class AuthMessageSender : IEmailSender, ISmsSender - { - public Task SendEmailAsync(string email, string subject, string message) - { - // Plug in your email service here to send an email. - return Task.FromResult(0); - } - - public Task SendSmsAsync(string number, string message) - { - // Plug in your SMS service here to send a text message. - return Task.FromResult(0); - } - } -} diff --git a/Sample/Startup.cs b/Sample/Startup.cs index 7b3d169..e621465 100644 --- a/Sample/Startup.cs +++ b/Sample/Startup.cs @@ -3,37 +3,25 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Sample.Web.NetCore.Models; -using Sample.Web.NetCore.Services; +using Sample.Models; +using Sample.Services; using RavenDB.Identity; -namespace Sample.Web.NetCore +namespace Sample { public class Startup { - public Startup(IHostingEnvironment env) + public Startup(IConfiguration configuration) { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); - - if (env.IsDevelopment()) - { - // For more details on using the user secret store see https://go.microsoft.com/fwlink/?LinkID=532709 - builder.AddUserSecrets(); - } - - builder.AddEnvironmentVariables(); - Configuration = builder.Build(); + Configuration = configuration; } - public IConfigurationRoot Configuration { get; } + public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) @@ -41,26 +29,24 @@ namespace Sample.Web.NetCore // Add RavenDB and identity. services .AddRavenDb(Configuration.GetConnectionString("RavenDbConnection")) // Create a RavenDB DocumentStore singleton. - .AddRavenDbAsyncSession() // Create a RavenDB IASyncDocumentSession for each request. + .AddRavenDbAsyncSession() // Create a RavenDB IAsyncDocumentSession for each request. .AddRavenDbIdentity(); // Use Raven for users and roles. - services.AddMvc(); // Add application services. - services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + + services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public void Configure(IApplicationBuilder app, IHostingEnvironment env) { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); + app.UseDatabaseErrorPage(); } else { @@ -69,9 +55,7 @@ namespace Sample.Web.NetCore app.UseStaticFiles(); - app.UseIdentity(); - - // Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715 + app.UseAuthentication(); app.UseMvc(routes => { diff --git a/Sample/Views/Account/AccessDenied.cshtml b/Sample/Views/Account/AccessDenied.cshtml index 9a6f8d5..3a5a008 100644 --- a/Sample/Views/Account/AccessDenied.cshtml +++ b/Sample/Views/Account/AccessDenied.cshtml @@ -1,8 +1,8 @@ @{ - ViewData["Title"] = "Access Denied"; + ViewData["Title"] = "Access denied"; }
-

Access Denied.

+

ViewData["Title"]

You do not have access to this resource.

diff --git a/Sample/Views/Account/ConfirmEmail.cshtml b/Sample/Views/Account/ConfirmEmail.cshtml index 3244fef..81125a4 100644 --- a/Sample/Views/Account/ConfirmEmail.cshtml +++ b/Sample/Views/Account/ConfirmEmail.cshtml @@ -1,10 +1,10 @@ @{ - ViewData["Title"] = "Confirm Email"; + ViewData["Title"] = "Confirm email"; } -

@ViewData["Title"].

+

@ViewData["Title"]

- Thank you for confirming your email. Please Click here to Log in. + Thank you for confirming your email.

diff --git a/Sample/Views/Account/ExternalLogin.cshtml b/Sample/Views/Account/ExternalLogin.cshtml new file mode 100644 index 0000000..d08d860 --- /dev/null +++ b/Sample/Views/Account/ExternalLogin.cshtml @@ -0,0 +1,32 @@ +@model ExternalLoginViewModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+

Associate your @ViewData["LoginProvider"] account.

+
+ +

+ You've successfully authenticated with @ViewData["LoginProvider"]. + Please enter an email address for this site below and click the Register button to finish + logging in. +

+ +
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/Sample/Views/Account/ExternalLoginConfirmation.cshtml b/Sample/Views/Account/ExternalLoginConfirmation.cshtml deleted file mode 100644 index 987fff4..0000000 --- a/Sample/Views/Account/ExternalLoginConfirmation.cshtml +++ /dev/null @@ -1,35 +0,0 @@ -@model ExternalLoginConfirmationViewModel -@{ - ViewData["Title"] = "Register"; -} - -

@ViewData["Title"].

-

Associate your @ViewData["LoginProvider"] account.

- -
-

Association Form

-
-
- -

- You've successfully authenticated with @ViewData["LoginProvider"]. - Please enter an email address for this site below and click the Register button to finish - logging in. -

-
- -
- - -
-
-
-
- -
-
-
- -@section Scripts { - @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } -} diff --git a/Sample/Views/Account/ExternalLoginFailure.cshtml b/Sample/Views/Account/ExternalLoginFailure.cshtml deleted file mode 100644 index d89339e..0000000 --- a/Sample/Views/Account/ExternalLoginFailure.cshtml +++ /dev/null @@ -1,8 +0,0 @@ -@{ - ViewData["Title"] = "Login Failure"; -} - -
-

@ViewData["Title"].

-

Unsuccessful login with service.

-
diff --git a/Sample/Views/Account/ForgotPassword.cshtml b/Sample/Views/Account/ForgotPassword.cshtml index 4cdc71c..c5a8e7c 100644 --- a/Sample/Views/Account/ForgotPassword.cshtml +++ b/Sample/Views/Account/ForgotPassword.cshtml @@ -4,28 +4,22 @@ }

@ViewData["Title"]

-

- For more information on how to enable reset password please see this article. -

- -@*
-

Enter your email.

-
-
-
- -
- - -
-
-
-
+

Enter your email.

+
+
+
+ +
+
+ + + +
-
+
-*@ +
@section Scripts { - @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } + @await Html.PartialAsync("_ValidationScriptsPartial") } diff --git a/Sample/Views/Account/ForgotPasswordConfirmation.cshtml b/Sample/Views/Account/ForgotPasswordConfirmation.cshtml index ab9bf44..ef1e54e 100644 --- a/Sample/Views/Account/ForgotPasswordConfirmation.cshtml +++ b/Sample/Views/Account/ForgotPasswordConfirmation.cshtml @@ -1,8 +1,8 @@ @{ - ViewData["Title"] = "Forgot Password Confirmation"; + ViewData["Title"] = "Forgot password confirmation"; } -

@ViewData["Title"].

+

@ViewData["Title"]

Please check your email to reset your password.

diff --git a/Sample/Views/Account/Lockout.cshtml b/Sample/Views/Account/Lockout.cshtml index 2cc946d..f573a1f 100644 --- a/Sample/Views/Account/Lockout.cshtml +++ b/Sample/Views/Account/Lockout.cshtml @@ -3,6 +3,6 @@ }
-

Locked out.

+

@ViewData["Title"]

This account has been locked out, please try again later.

diff --git a/Sample/Views/Account/Login.cshtml b/Sample/Views/Account/Login.cshtml index e45e6f8..171819b 100644 --- a/Sample/Views/Account/Login.cshtml +++ b/Sample/Views/Account/Login.cshtml @@ -1,4 +1,5 @@ @using System.Collections.Generic +@using System.Linq @using Microsoft.AspNetCore.Http @using Microsoft.AspNetCore.Http.Authentication @model LoginViewModel @@ -8,58 +9,52 @@ ViewData["Title"] = "Log in"; } -

@ViewData["Title"].

+

@ViewData["Title"]

-
+
-
+

Use a local account to log in.


- -
- - + + + +
+
+ + + +
+
+
+
- -
- - -
+
-
-
- -
-
+

+ Forgot your password? +

+

+ Register as a new user? +

-
-
- -
-
-

- Register as a new user? -

-

- Forgot your password? -

-
+

Use another service to log in.


@{ - var loginProviders = SignInManager.GetExternalAuthenticationSchemes().ToList(); + var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); if (loginProviders.Count == 0) {
@@ -71,12 +66,12 @@ } else { -
+

@foreach (var provider in loginProviders) { - + }

@@ -88,5 +83,5 @@
@section Scripts { - @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } + @await Html.PartialAsync("_ValidationScriptsPartial") } diff --git a/Sample/Views/Account/LoginWith2fa.cshtml b/Sample/Views/Account/LoginWith2fa.cshtml new file mode 100644 index 0000000..0f554e2 --- /dev/null +++ b/Sample/Views/Account/LoginWith2fa.cshtml @@ -0,0 +1,40 @@ +@model LoginWith2faViewModel +@{ + ViewData["Title"] = "Two-factor authentication"; +} + +

@ViewData["Title"]

+
+

Your login is protected with an authenticator app. Enter your authenticator code below.

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

+ Don't have access to your authenticator device? You can + log in with a recovery code. +

+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} \ No newline at end of file diff --git a/Sample/Views/Account/LoginWithRecoveryCode.cshtml b/Sample/Views/Account/LoginWithRecoveryCode.cshtml new file mode 100644 index 0000000..e2eb4c5 --- /dev/null +++ b/Sample/Views/Account/LoginWithRecoveryCode.cshtml @@ -0,0 +1,28 @@ +@model LoginWithRecoveryCodeViewModel +@{ + ViewData["Title"] = "Recovery code verification"; +} + +

@ViewData["Title"]

+
+

+ You have requested to login with a recovery code. This login will not be remembered until you provide + an authenticator app code at login or disable 2FA and login again. +

+
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} \ No newline at end of file diff --git a/Sample/Views/Account/Register.cshtml b/Sample/Views/Account/Register.cshtml index dad5ea2..05c6676 100644 --- a/Sample/Views/Account/Register.cshtml +++ b/Sample/Views/Account/Register.cshtml @@ -3,40 +3,34 @@ ViewData["Title"] = "Register"; } -

@ViewData["Title"].

+

@ViewData["Title"]

-
-

Create a new account.

-
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
-
+
+
+ +

Create a new account.

+
+
+
+ + + +
+
+ + + +
+
+ + + +
-
+
- +
@section Scripts { - @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } + @await Html.PartialAsync("_ValidationScriptsPartial") } diff --git a/Sample/Views/Account/ResetPassword.cshtml b/Sample/Views/Account/ResetPassword.cshtml index 6e584da..d55c679 100644 --- a/Sample/Views/Account/ResetPassword.cshtml +++ b/Sample/Views/Account/ResetPassword.cshtml @@ -3,41 +3,34 @@ ViewData["Title"] = "Reset password"; } -

@ViewData["Title"].

- -
-

Reset your password.

-
-
- -
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
-
+

@ViewData["Title"]

+

Reset your password.

+
+
+
+ +
+ +
+ + + +
+
+ + + +
+
+ + + +
-
+
- +
@section Scripts { - @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } + @await Html.PartialAsync("_ValidationScriptsPartial") } diff --git a/Sample/Views/Account/ResetPasswordConfirmation.cshtml b/Sample/Views/Account/ResetPasswordConfirmation.cshtml index bef2e45..7967cf4 100644 --- a/Sample/Views/Account/ResetPasswordConfirmation.cshtml +++ b/Sample/Views/Account/ResetPasswordConfirmation.cshtml @@ -2,7 +2,7 @@ ViewData["Title"] = "Reset password confirmation"; } -

@ViewData["Title"].

+

@ViewData["Title"]

- Your password has been reset. Please Click here to log in. + Your password has been reset. Please click here to log in.

diff --git a/Sample/Views/Account/SendCode.cshtml b/Sample/Views/Account/SendCode.cshtml deleted file mode 100644 index 8621ce7..0000000 --- a/Sample/Views/Account/SendCode.cshtml +++ /dev/null @@ -1,21 +0,0 @@ -@model SendCodeViewModel -@{ - ViewData["Title"] = "Send Verification Code"; -} - -

@ViewData["Title"].

- -
- -
-
- Select Two-Factor Authentication Provider: - - -
-
-
- -@section Scripts { - @{await Html.RenderPartialAsync("_ValidationScriptsPartial"); } -} diff --git a/Sample/Views/Account/SignedOut.cshtml b/Sample/Views/Account/SignedOut.cshtml new file mode 100644 index 0000000..c7fb988 --- /dev/null +++ b/Sample/Views/Account/SignedOut.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Signed out"; +} + +

@ViewData["Title"]

+

+ You have successfully signed out. +

diff --git a/Sample/Views/Account/VerifyCode.cshtml b/Sample/Views/Account/VerifyCode.cshtml deleted file mode 100644 index 52a4c73..0000000 --- a/Sample/Views/Account/VerifyCode.cshtml +++ /dev/null @@ -1,38 +0,0 @@ -@model VerifyCodeViewModel -@{ - ViewData["Title"] = "Verify"; -} - -

@ViewData["Title"].

- -
-
- - -

@ViewData["Status"]

-
-
- -
- - -
-
-
-
-
- - -
-
-
-
-
- -
-
-
- -@section Scripts { - @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } -} diff --git a/Sample/Views/Home/About.cshtml b/Sample/Views/Home/About.cshtml index 50476d1..3674e37 100644 --- a/Sample/Views/Home/About.cshtml +++ b/Sample/Views/Home/About.cshtml @@ -1,7 +1,7 @@ @{ ViewData["Title"] = "About"; } -

@ViewData["Title"].

+

@ViewData["Title"]

@ViewData["Message"]

Use this area to provide additional information.

diff --git a/Sample/Views/Home/Contact.cshtml b/Sample/Views/Home/Contact.cshtml index 15c12c6..a11a186 100644 --- a/Sample/Views/Home/Contact.cshtml +++ b/Sample/Views/Home/Contact.cshtml @@ -1,7 +1,7 @@ @{ ViewData["Title"] = "Contact"; } -

@ViewData["Title"].

+

@ViewData["Title"]

@ViewData["Message"]

diff --git a/Sample/Views/Home/Index.cshtml b/Sample/Views/Home/Index.cshtml index 235978c..e847eeb 100644 --- a/Sample/Views/Home/Index.cshtml +++ b/Sample/Views/Home/Index.cshtml @@ -54,9 +54,10 @@
diff --git a/Sample/Views/Manage/AddPhoneNumber.cshtml b/Sample/Views/Manage/AddPhoneNumber.cshtml deleted file mode 100644 index 5e96c6e..0000000 --- a/Sample/Views/Manage/AddPhoneNumber.cshtml +++ /dev/null @@ -1,27 +0,0 @@ -@model AddPhoneNumberViewModel -@{ - ViewData["Title"] = "Add Phone Number"; -} - -

@ViewData["Title"].

-
-

Add a phone number.

-
-
-
- -
- - -
-
-
-
- -
-
-
- -@section Scripts { - @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } -} diff --git a/Sample/Views/Manage/ChangePassword.cshtml b/Sample/Views/Manage/ChangePassword.cshtml index d19ef1d..0f7b041 100644 --- a/Sample/Views/Manage/ChangePassword.cshtml +++ b/Sample/Views/Manage/ChangePassword.cshtml @@ -1,42 +1,35 @@ @model ChangePasswordViewModel @{ - ViewData["Title"] = "Change Password"; + ViewData["Title"] = "Change password"; + ViewData.AddActivePage(ManageNavPages.ChangePassword); } -

@ViewData["Title"].

- -
-

Change Password Form

-
-
-
- -
- - -
+

@ViewData["Title"]

+@Html.Partial("_StatusMessage", Model.StatusMessage) +
+
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+ +
-
- -
- - -
-
-
- -
- - -
-
-
-
- -
-
- +
@section Scripts { - @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } + @await Html.PartialAsync("_ValidationScriptsPartial") } diff --git a/Sample/Views/Manage/Disable2fa.cshtml b/Sample/Views/Manage/Disable2fa.cshtml new file mode 100644 index 0000000..cdd79cb --- /dev/null +++ b/Sample/Views/Manage/Disable2fa.cshtml @@ -0,0 +1,24 @@ +@{ + ViewData["Title"] = "Disable two-factor authentication (2FA)"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

@ViewData["Title"]

+ + + +
+
+ +
+
diff --git a/Sample/Views/Manage/EnableAuthenticator.cshtml b/Sample/Views/Manage/EnableAuthenticator.cshtml new file mode 100644 index 0000000..79693d7 --- /dev/null +++ b/Sample/Views/Manage/EnableAuthenticator.cshtml @@ -0,0 +1,52 @@ +@model EnableAuthenticatorViewModel +@{ + ViewData["Title"] = "Enable authenticator"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

@ViewData["Title"]

+
+

To use an authenticator app go through the following steps:

+
    +
  1. +

    + Download a two-factor authenticator app like Microsoft Authenticator for + Windows Phone, + Android and + iOS or + Google Authenticator for + Android and + iOS. +

    +
  2. +
  3. +

    Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.

    +
    To enable QR code generation please read our documentation.
    +
    +
    +
  4. +
  5. +

    + Once you have scanned the QR code or input the key above, your two factor authentication app will provide you + with a unique code. Enter the code in the confirmation box below. +

    +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    +
  6. +
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/Sample/Views/Manage/ExternalLogins.cshtml b/Sample/Views/Manage/ExternalLogins.cshtml new file mode 100644 index 0000000..e6f5687 --- /dev/null +++ b/Sample/Views/Manage/ExternalLogins.cshtml @@ -0,0 +1,52 @@ +@model ExternalLoginsViewModel +@{ + ViewData["Title"] = "Manage your external logins"; + ViewData.AddActivePage(ManageNavPages.ExternalLogins); +} + +@Html.Partial("_StatusMessage", Model.StatusMessage) +@if (Model.CurrentLogins?.Count > 0) +{ +

Registered Logins

+ + + @foreach (var login in Model.CurrentLogins) + { + + + + + } + +
@login.LoginProvider + @if (Model.ShowRemoveButton) + { +
+
+ + + +
+
+ } + else + { + @:   + } +
+} +@if (Model.OtherLogins?.Count > 0) +{ +

Add another service to log in.

+
+
+
+

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

+
+
+} diff --git a/Sample/Views/Manage/GenerateRecoveryCodes.cshtml b/Sample/Views/Manage/GenerateRecoveryCodes.cshtml new file mode 100644 index 0000000..669d13e --- /dev/null +++ b/Sample/Views/Manage/GenerateRecoveryCodes.cshtml @@ -0,0 +1,24 @@ +@model GenerateRecoveryCodesViewModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

@ViewData["Title"]

+ +
+
+ @for (var row = 0; row < Model.RecoveryCodes.Count(); row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ } +
+
\ No newline at end of file diff --git a/Sample/Views/Manage/Index.cshtml b/Sample/Views/Manage/Index.cshtml index 4f49441..c141c95 100644 --- a/Sample/Views/Manage/Index.cshtml +++ b/Sample/Views/Manage/Index.cshtml @@ -1,71 +1,45 @@ @model IndexViewModel @{ - ViewData["Title"] = "Manage your account"; + ViewData["Title"] = "Profile"; + ViewData.AddActivePage(ManageNavPages.Index); } -

@ViewData["Title"].

-

@ViewData["StatusMessage"]

- -
-

Change your account settings

-
-
-
Password:
-
- @if (Model.HasPassword) - { - Change - } - else - { - Create - } -
-
External Logins:
-
- - @Model.Logins.Count Manage -
-
Phone Number:
-
-

- Phone Numbers can be used as a second factor of verification in two-factor authentication. - See this article - for details on setting up this ASP.NET application to support two-factor authentication using SMS. -

- @*@(Model.PhoneNumber ?? "None") - @if (Model.PhoneNumber != null) +

@ViewData["Title"]

+@Html.Partial("_StatusMessage", Model.StatusMessage) +
+
+
+
+
+ + +
+
+ + @if (Model.IsEmailConfirmed) { -
- Change - - [] - +
+ + +
} else { - Add - }*@ -
- -
Two-Factor Authentication:
-
-

- There are no two-factor authentication providers configured. See this article - for setting up this application to support two-factor authentication. -

- @*@if (Model.TwoFactor) - { -
- Enabled -
+ + } - else - { -
- Disabled -
- }*@ -
-
+ +
+
+ + + +
+ + +
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/Sample/Views/Manage/ManageLogins.cshtml b/Sample/Views/Manage/ManageLogins.cshtml deleted file mode 100644 index cb764d4..0000000 --- a/Sample/Views/Manage/ManageLogins.cshtml +++ /dev/null @@ -1,54 +0,0 @@ -@model ManageLoginsViewModel -@using Microsoft.AspNetCore.Http.Authentication -@{ - ViewData["Title"] = "Manage your external logins"; -} - -

@ViewData["Title"].

- -

@ViewData["StatusMessage"]

-@if (Model.CurrentLogins.Count > 0) -{ -

Registered Logins

- - - @for (var index = 0; index < Model.CurrentLogins.Count; index++) - { - - - - - } - -
@Model.CurrentLogins[index].LoginProvider - @if ((bool)ViewData["ShowRemoveButton"]) - { -
-
- - - -
-
- } - else - { - @:   - } -
-} -@if (Model.OtherLogins.Count > 0) -{ -

Add another service to log in.

-
-
-
-

- @foreach (var provider in Model.OtherLogins) - { - - } -

-
-
-} diff --git a/Sample/Views/Manage/ManageNavPages.cs b/Sample/Views/Manage/ManageNavPages.cs new file mode 100644 index 0000000..69cb68a --- /dev/null +++ b/Sample/Views/Manage/ManageNavPages.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace Sample.Views.Manage +{ + public static class ManageNavPages + { + public static string ActivePageKey => "ActivePage"; + + public static string Index => "Index"; + + public static string ChangePassword => "ChangePassword"; + + public static string ExternalLogins => "ExternalLogins"; + + public static string TwoFactorAuthentication => "TwoFactorAuthentication"; + + public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + + public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); + + public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); + + public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); + + public static string PageNavClass(ViewContext viewContext, string page) + { + var activePage = viewContext.ViewData["ActivePage"] as string; + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; + } + + public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage; + } +} diff --git a/Sample/Views/Manage/ResetAuthenticator.cshtml b/Sample/Views/Manage/ResetAuthenticator.cshtml new file mode 100644 index 0000000..54a2361 --- /dev/null +++ b/Sample/Views/Manage/ResetAuthenticator.cshtml @@ -0,0 +1,21 @@ +@{ + ViewData["Title"] = "Reset authenticator key"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

@ViewData["Title"]

+ +
+
+ +
+
\ No newline at end of file diff --git a/Sample/Views/Manage/SetPassword.cshtml b/Sample/Views/Manage/SetPassword.cshtml index 8f832ed..56c3599 100644 --- a/Sample/Views/Manage/SetPassword.cshtml +++ b/Sample/Views/Manage/SetPassword.cshtml @@ -1,38 +1,34 @@ @model SetPasswordViewModel @{ - ViewData["Title"] = "Set Password"; + ViewData["Title"] = "Set password"; + ViewData.AddActivePage(ManageNavPages.ChangePassword); } +

Set your password

+@Html.Partial("_StatusMessage", Model.StatusMessage)

You do not have a local username/password for this site. Add a local account so you can log in without an external login.

- -
-

Set your password

-
-
-
- -
- - -
-
-
- -
- - -
-
-
-
+
+
+ +
+
+ + + +
+
+ + + +
-
+
- +
@section Scripts { - @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } + @await Html.PartialAsync("_ValidationScriptsPartial") } diff --git a/Sample/Views/Manage/TwoFactorAuthentication.cshtml b/Sample/Views/Manage/TwoFactorAuthentication.cshtml new file mode 100644 index 0000000..a2b52ac --- /dev/null +++ b/Sample/Views/Manage/TwoFactorAuthentication.cshtml @@ -0,0 +1,49 @@ +@model TwoFactorAuthenticationViewModel +@{ + ViewData["Title"] = "Two-factor authentication"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

@ViewData["Title"]

+@if (Model.Is2faEnabled) +{ + if (Model.RecoveryCodesLeft == 0) + { +
+ You have no recovery codes left. +

You must generate a new set of recovery codes before you can log in with a recovery code.

+
+ } + else if (Model.RecoveryCodesLeft == 1) + { +
+ You have 1 recovery code left. +

You can generate a new set of recovery codes.

+
+ } + else if (Model.RecoveryCodesLeft <= 3) + { +
+ You have @Model.RecoveryCodesLeft recovery codes left. +

You should generate a new set of recovery codes.

+
+ } + + Disable 2FA + Reset recovery codes +} + +
Authenticator app
+@if (!Model.HasAuthenticator) +{ + Add authenticator app +} +else +{ + Configure authenticator app + Reset authenticator key +} + +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/Sample/Views/Manage/VerifyPhoneNumber.cshtml b/Sample/Views/Manage/VerifyPhoneNumber.cshtml deleted file mode 100644 index e8b460e..0000000 --- a/Sample/Views/Manage/VerifyPhoneNumber.cshtml +++ /dev/null @@ -1,30 +0,0 @@ -@model VerifyPhoneNumberViewModel -@{ - ViewData["Title"] = "Verify Phone Number"; -} - -

@ViewData["Title"].

- -
- -

Add a phone number.

-
@ViewData["Status"]
-
-
-
- -
- - -
-
-
-
- -
-
-
- -@section Scripts { - @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } -} diff --git a/Sample/Views/Manage/_Layout.cshtml b/Sample/Views/Manage/_Layout.cshtml new file mode 100644 index 0000000..95dc87d --- /dev/null +++ b/Sample/Views/Manage/_Layout.cshtml @@ -0,0 +1,23 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} + +

Manage your account

+ +
+

Change your account settings

+
+
+
+ @await Html.PartialAsync("_ManageNav") +
+
+ @RenderBody() +
+
+
+ +@section Scripts { + @RenderSection("Scripts", required: false) +} + diff --git a/Sample/Views/Manage/_ManageNav.cshtml b/Sample/Views/Manage/_ManageNav.cshtml new file mode 100644 index 0000000..7da64af --- /dev/null +++ b/Sample/Views/Manage/_ManageNav.cshtml @@ -0,0 +1,16 @@ +@using Sample.Views.Manage +@inject SignInManager SignInManager +@{ + var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); +} + + + diff --git a/Sample/Views/Manage/_StatusMessage.cshtml b/Sample/Views/Manage/_StatusMessage.cshtml new file mode 100644 index 0000000..e996841 --- /dev/null +++ b/Sample/Views/Manage/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} diff --git a/Sample/Views/Manage/_ViewImports.cshtml b/Sample/Views/Manage/_ViewImports.cshtml new file mode 100644 index 0000000..0761f13 --- /dev/null +++ b/Sample/Views/Manage/_ViewImports.cshtml @@ -0,0 +1 @@ +@using Sample.Views.Manage \ No newline at end of file diff --git a/Sample/Views/Shared/Error.cshtml b/Sample/Views/Shared/Error.cshtml index e514139..ec2ea6b 100644 --- a/Sample/Views/Shared/Error.cshtml +++ b/Sample/Views/Shared/Error.cshtml @@ -1,10 +1,18 @@ -@{ +@model ErrorViewModel +@{ ViewData["Title"] = "Error"; }

Error.

An error occurred while processing your request.

+@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} +

Development Mode

Swapping to Development environment will display more detailed information about the error that occurred. diff --git a/Sample/Views/Shared/_Layout.cshtml b/Sample/Views/Shared/_Layout.cshtml index 0f3e1de..ad01061 100644 --- a/Sample/Views/Shared/_Layout.cshtml +++ b/Sample/Views/Shared/_Layout.cshtml @@ -1,22 +1,20 @@ -@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet - + - @ViewData["Title"] - Sample Asp.NET Core app with RavenDB Identity + @ViewData["Title"] - Sample - + - + - @Html.Raw(JavaScriptSnippet.FullScript)

- + - + - +