From 9f4d46714b6655c98593dc50cffade382286859f Mon Sep 17 00:00:00 2001 From: Praburaj Date: Thu, 28 Aug 2014 16:57:43 -0700 Subject: [PATCH] Updating the sample to use VS 2013 Update 3 templates for Identity 1. This contains the new manage controller with a bunch of features like 2 factor auth Some of the features are not enabled yet. But this is to set a baseline with update3 templates. Automation to come up in future as more identity extensions are available. --- .../Controllers/AccountController.cs | 357 +++++++++++++++--- .../Controllers/ManageController.cs | 348 +++++++++++++++++ src/MusicStore/LocalConfig.json | 2 +- src/MusicStore/Models/AccountViewModels.cs | 88 ++++- src/MusicStore/Models/ManageViewModels.cs | 88 +++++ src/MusicStore/MusicStore.kproj | 2 - src/MusicStore/Startup.cs | 10 +- .../Views/Account/ConfirmEmail.cshtml | 10 + .../Account/ExternalLoginConfirmation.cshtml | 39 ++ .../Views/Account/ExternalLoginFailure.cshtml | 8 + .../Views/Account/ForgotPassword.cshtml | 32 ++ .../Account/ForgotPasswordConfirmation.cshtml | 15 + src/MusicStore/Views/Account/Login.cshtml | 24 +- src/MusicStore/Views/Account/Manage.cshtml | 19 - src/MusicStore/Views/Account/Register.cshtml | 6 +- .../Views/Account/RegisterConfirmation.cshtml | 15 + .../Views/Account/ResetPassword.cshtml | 45 +++ .../Account/ResetPasswordConfirmation.cshtml | 12 + src/MusicStore/Views/Account/SendCode.cshtml | 27 ++ .../Account/_ExternalLoginsListPartial.cshtml | 28 ++ .../Views/Manage/AddPhoneNumber.cshtml | 32 ++ .../ChangePassword.cshtml} | 19 +- src/MusicStore/Views/Manage/Index.cshtml | 106 ++++++ .../Views/Manage/ManageLogins.cshtml | 58 +++ .../Views/Manage/SetPassword.cshtml | 43 +++ .../Views/Manage/VerifyPhoneNumber.cshtml | 34 ++ .../Views/Shared/DemoCodeDisplay.cshtml | 15 + src/MusicStore/Views/Shared/Lockout.cshtml | 8 + .../Views/Shared/_LoginPartial.cshtml | 2 +- test/E2ETests/SmokeTests.cs | 96 ++--- 30 files changed, 1429 insertions(+), 159 deletions(-) create mode 100644 src/MusicStore/Controllers/ManageController.cs create mode 100644 src/MusicStore/Models/ManageViewModels.cs create mode 100644 src/MusicStore/Views/Account/ConfirmEmail.cshtml create mode 100644 src/MusicStore/Views/Account/ExternalLoginConfirmation.cshtml create mode 100644 src/MusicStore/Views/Account/ExternalLoginFailure.cshtml create mode 100644 src/MusicStore/Views/Account/ForgotPassword.cshtml create mode 100644 src/MusicStore/Views/Account/ForgotPasswordConfirmation.cshtml delete mode 100644 src/MusicStore/Views/Account/Manage.cshtml create mode 100644 src/MusicStore/Views/Account/RegisterConfirmation.cshtml create mode 100644 src/MusicStore/Views/Account/ResetPassword.cshtml create mode 100644 src/MusicStore/Views/Account/ResetPasswordConfirmation.cshtml create mode 100644 src/MusicStore/Views/Account/SendCode.cshtml create mode 100644 src/MusicStore/Views/Account/_ExternalLoginsListPartial.cshtml create mode 100644 src/MusicStore/Views/Manage/AddPhoneNumber.cshtml rename src/MusicStore/Views/{Account/_ChangePasswordPartial.cshtml => Manage/ChangePassword.cshtml} (62%) create mode 100644 src/MusicStore/Views/Manage/Index.cshtml create mode 100644 src/MusicStore/Views/Manage/ManageLogins.cshtml create mode 100644 src/MusicStore/Views/Manage/SetPassword.cshtml create mode 100644 src/MusicStore/Views/Manage/VerifyPhoneNumber.cshtml create mode 100644 src/MusicStore/Views/Shared/DemoCodeDisplay.cshtml create mode 100644 src/MusicStore/Views/Shared/Lockout.cshtml diff --git a/src/MusicStore/Controllers/AccountController.cs b/src/MusicStore/Controllers/AccountController.cs index c615d52754..015fec7626 100644 --- a/src/MusicStore/Controllers/AccountController.cs +++ b/src/MusicStore/Controllers/AccountController.cs @@ -1,8 +1,11 @@ -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Mvc; -using MusicStore.Models; +using System.Linq; using System.Security.Principal; using System.Threading.Tasks; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.Rendering; +using MusicStore.Models; namespace MusicStore.Controllers { @@ -35,25 +38,78 @@ namespace MusicStore.Controllers [ValidateAntiForgeryToken] public async Task Login(LoginViewModel model, string returnUrl = null) { - if (ModelState.IsValid) + if (!ModelState.IsValid) { - var signInStatus = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false); - switch (signInStatus) - { - case SignInStatus.Success: - return RedirectToLocal(returnUrl); - case SignInStatus.LockedOut: - ModelState.AddModelError("", "User is locked out, try again later."); - return View(model); - case SignInStatus.Failure: - default: - ModelState.AddModelError("", "Invalid username or password."); - return View(model); - } + return View(model); } - // If we got this far, something failed, redisplay form - return View(model); + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, change to shouldLockout: true + var signInStatus = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false); + switch (signInStatus) + { + case SignInStatus.Success: + return RedirectToLocal(returnUrl); + case SignInStatus.LockedOut: + return View("Lockout"); + case SignInStatus.RequiresVerification: + return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); + case SignInStatus.Failure: + default: + ModelState.AddModelError("", "Invalid login attempt."); + return View(model); + } + } + + // + // GET: /Account/VerifyCode + //TODO : Some of the identity helpers not implemented + //[AllowAnonymous] + //public async Task VerifyCode(string provider, string returnUrl, bool rememberMe) + //{ + // // Require that the user has already logged in via username/password or external login + // if (!await SignInManager.HasBeenVerifiedAsync()) + // { + // return View("Error"); + // } + // var user = await UserManager.FindByIdAsync(await SignInManager.GetVerifiedUserIdAsync()); + // if (user != null) + // { + // var code = await UserManager.GenerateTwoFactorTokenAsync(user.Id, provider); + // } + // 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. + // You can configure the account lockout settings in IdentityConfig + // TODO : This helper does not take in the remember browser option yet. + // var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent: model.RememberMe, rememberBrowser: model.RememberBrowser); + var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent: model.RememberMe); + switch (result) + { + case SignInStatus.Success: + return RedirectToLocal(model.ReturnUrl); + case SignInStatus.LockedOut: + return View("Lockout"); + case SignInStatus.Failure: + default: + ModelState.AddModelError("", "Invalid code."); + return View(model); + } } // @@ -73,17 +129,26 @@ namespace MusicStore.Controllers { if (ModelState.IsValid) { - var user = new ApplicationUser { UserName = model.UserName }; + var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { + //Bug: Remember browser option missing? await SignInManager.SignInAsync(user, isPersistent: false); + + // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771 + // Send an email with this link + //string code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + //var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Context.Request.Scheme); + // await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking here"); + // return RedirectToAction("Index", "Home"); + + // TODO: Email libraries not available on Coreclr yet - Checkout a demo implementation below which displays the code in the browser for testing. + //ViewBag.Status = "Navigate to this URL to confirm your account " + callbackUrl; + //return View("DemoCodeDisplay"); return RedirectToAction("Index", "Home"); } - else - { - AddErrors(result); - } + AddErrors(result); } // If we got this far, something failed, redisplay form @@ -91,42 +156,234 @@ namespace MusicStore.Controllers } // - // GET: /Account/Manage - public IActionResult Manage(ManageMessageId? message = null) + // GET: /Account/ConfirmEmail + //TODO: This does not work yet due to some missing identity features + [AllowAnonymous] + public async Task ConfirmEmail(string userId, string code) + { + if (userId == null || code == null) + { + return View("Error"); + } + var user = new ApplicationUser { Id = userId }; + //Bug: Throws NullRefException + var result = await UserManager.ConfirmEmailAsync(user, code); + return View(result.Succeeded ? "ConfirmEmail" : "Error"); + } + + // + // GET: /Account/ForgotPassword + [AllowAnonymous] + public ActionResult ForgotPassword() { - ViewBag.StatusMessage = - message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." - : message == ManageMessageId.Error ? "An error has occurred." - : ""; - ViewBag.ReturnUrl = Url.Action("Manage"); return View(); } // - // POST: /Account/Manage + // POST: /Account/ForgotPassword [HttpPost] + [AllowAnonymous] [ValidateAntiForgeryToken] - public async Task Manage(ManageUserViewModel model) + public async Task ForgotPassword(ForgotPasswordViewModel model) { - ViewBag.ReturnUrl = Url.Action("Manage"); if (ModelState.IsValid) { - var user = await GetCurrentUserAsync(); - var result = await UserManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); - if (result.Succeeded) + var user = await UserManager.FindByNameAsync(model.Email); + if (user == null || !(await UserManager.IsEmailConfirmedAsync(user))) { - return RedirectToAction("Manage", new { Message = ManageMessageId.ChangePasswordSuccess }); - } - else - { - AddErrors(result); + // Don't reveal that the user does not exist or is not confirmed + return View("ForgotPasswordConfirmation"); } + + // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771 + // Send an email with this link + string code = await UserManager.GeneratePasswordResetTokenAsync(user); + var callbackUrl = Url.Action("ResetPassword", "Account", new { code = code }, protocol: Context.Request.Scheme); + // No libraries to send email on CoreCLR yet + // await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking here"); + // return RedirectToAction("ForgotPasswordConfirmation", "Account"); + + // TODO: Email libraries not available on Coreclr yet - Checkout a demo implementation below which displays the code in the browser for testing. + ViewBag.Status = "Navigate to the URL to reset password " + callbackUrl; + return View("DemoCodeDisplay"); } + ModelState.AddModelError("", string.Format("We could not locate an account with email : {0}", model.Email)); + // If we got this far, something failed, redisplay form return View(model); } + // + // GET: /Account/ForgotPasswordConfirmation + [AllowAnonymous] + public ActionResult ForgotPasswordConfirmation() + { + return View(); + } + + // + // GET: /Account/ResetPassword + [AllowAnonymous] + public ActionResult ResetPassword(string code) + { + var resetPasswordViewModel = new ResetPasswordViewModel() { Code = code }; + return code == null ? View("Error") : View(resetPasswordViewModel); + } + + // + // POST: /Account/ResetPassword + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ResetPassword(ResetPasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await UserManager.FindByNameAsync(model.Email); + if (user == null) + { + // Don't reveal that the user does not exist + return RedirectToAction("ResetPasswordConfirmation", "Account"); + } + var result = await UserManager.ResetPasswordAsync(user, model.Code, model.Password); + if (result.Succeeded) + { + return RedirectToAction("ResetPasswordConfirmation", "Account"); + } + AddErrors(result); + return View(); + } + + // + // GET: /Account/ResetPasswordConfirmation + [AllowAnonymous] + public ActionResult ResetPasswordConfirmation() + { + return View(); + } + + // + // POST: /Account/ExternalLogin + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public ActionResult ExternalLogin(string provider, string returnUrl) + { + // Request a redirect to the external login provider + var redirectUri = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }); + return new ChallengeResult(provider, new AuthenticationProperties() { RedirectUri = redirectUri }); + } + + // + // GET: /Account/SendCode + [AllowAnonymous] + public async Task SendCode(bool rememberMe, string returnUrl = null) + { + // TODO: This currently throws + var userId = await GetCurrentUserAsync(); + if (userId == null) + { + return View("Error"); + } + var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(userId); + 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(); + } + + // Generate the token and send it + if (!await SignInManager.SendTwoFactorCodeAsync(model.SelectedProvider)) + { + return View("Error"); + } + return RedirectToAction("VerifyCode", new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe }); + } + + // + // GET: /Account/ExternalLoginCallback + // TODO: Some identity helpers on external login does not exist + //[AllowAnonymous] + //public async Task ExternalLoginCallback(string returnUrl) + //{ + // var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(); + // if (loginInfo == null) + // { + // return RedirectToAction("Login"); + // } + + // // Sign in the user with this external login provider if the user already has a login + // var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false); + // switch (result) + // { + // case SignInStatus.Success: + // return RedirectToLocal(returnUrl); + // case SignInStatus.LockedOut: + // return View("Lockout"); + // case SignInStatus.RequiresVerification: + // return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false }); + // case SignInStatus.Failure: + // default: + // // If the user does not have an account, then prompt the user to create an account + // ViewBag.ReturnUrl = returnUrl; + // ViewBag.LoginProvider = loginInfo.Login.LoginProvider; + // return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email }); + // } + //} + + // + // POST: /Account/ExternalLoginConfirmation + // TODO: Some of the identity helpers not available + //[HttpPost] + //[AllowAnonymous] + //[ValidateAntiForgeryToken] + //public async Task ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl) + //{ + // if (User.Identity.IsAuthenticated) + // { + // return RedirectToAction("Index", "Manage"); + // } + + // if (ModelState.IsValid) + // { + // // Get the information about the user from the external login provider + // var info = await AuthenticationManager.GetExternalLoginInfoAsync(); + // if (info == null) + // { + // return View("ExternalLoginFailure"); + // } + // var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; + // var result = await UserManager.CreateAsync(user); + // if (result.Succeeded) + // { + // result = await UserManager.AddLoginAsync(user.Id, info.Login); + // if (result.Succeeded) + // { + // // TODO: rememberBrowser option not being taken in SignInAsync + // await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); + // return RedirectToLocal(returnUrl); + // } + // } + // AddErrors(result); + // } + + // ViewBag.ReturnUrl = returnUrl; + // return View(model); + //} + // // POST: /Account/LogOff [HttpPost] @@ -137,6 +394,14 @@ namespace MusicStore.Controllers return RedirectToAction("Index", "Home"); } + // + // GET: /Account/ExternalLoginFailure + [AllowAnonymous] + public ActionResult ExternalLoginFailure() + { + return View(); + } + #region Helpers private void AddErrors(IdentityResult result) @@ -152,13 +417,7 @@ namespace MusicStore.Controllers return await UserManager.FindByIdAsync(Context.User.Identity.GetUserId()); } - public enum ManageMessageId - { - ChangePasswordSuccess, - Error - } - - private IActionResult RedirectToLocal(string returnUrl) + private ActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { diff --git a/src/MusicStore/Controllers/ManageController.cs b/src/MusicStore/Controllers/ManageController.cs new file mode 100644 index 0000000000..74a054ae02 --- /dev/null +++ b/src/MusicStore/Controllers/ManageController.cs @@ -0,0 +1,348 @@ +using System; +using System.Threading.Tasks; +using System.Security.Principal; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Mvc; +using MusicStore.Models; + +namespace MusicStore.Controllers +{ + /// + /// Summary description for ManageController + /// + public class ManageController : Controller + { + public ManageController(UserManager userManager, SignInManager signInManager) + { + UserManager = userManager; + SignInManager = signInManager; + } + + public UserManager UserManager { get; private set; } + + public SignInManager SignInManager { get; private set; } + + // + // GET: /Manage/Index + public async Task Index(ManageMessageId? message = null) + { + ViewBag.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." + : ""; + + var user = await GetCurrentUserAsync(); + 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) + }; + + return View(model); + } + + // + // POST: /Manage/RemoveLogin + [HttpPost] + [ValidateAntiForgeryToken] + public async Task RemoveLogin(string loginProvider, string providerKey) + { + ManageMessageId? message = ManageMessageId.Error; + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await UserManager.RemoveLoginAsync(user, loginProvider, providerKey); + if (result.Succeeded) + { + await SignInManager.SignInAsync(user, isPersistent: false); + message = ManageMessageId.RemoveLoginSuccess; + } + } + return RedirectToAction("ManageLogins", new { Message = message }); + } + + // + // GET: /Account/AddPhoneNumber + public IActionResult AddPhoneNumber() + { + return View(); + } + + // + // POST: /Account/AddPhoneNumber + [HttpPost] + [ValidateAntiForgeryToken] + public async Task AddPhoneNumber(AddPhoneNumberViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + // Generate the token and send it + var code = await UserManager.GenerateChangePhoneNumberTokenAsync(await GetCurrentUserAsync(), model.Number); + if (UserManager.SmsService != null) + { + var message = new IdentityMessage + { + Destination = model.Number, + Body = "Your security code is: " + code + }; + await UserManager.SmsService.SendAsync(message); + } + + return RedirectToAction("VerifyPhoneNumber", new { PhoneNumber = model.Number }); + } + + // + // POST: /Manage/EnableTwoFactorAuthentication + [HttpPost] + [ValidateAntiForgeryToken] + public async Task EnableTwoFactorAuthentication() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + await UserManager.SetTwoFactorEnabledAsync(user, true); + // TODO: flow remember me somehow? + await SignInManager.SignInAsync(user, isPersistent: false); + } + return RedirectToAction("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); + } + return RedirectToAction("Index", "Manage"); + } + + // + // GET: /Account/VerifyPhoneNumber + public async Task VerifyPhoneNumber(string phoneNumber) + { + // This code allows you exercise the flow without actually sending codes + // For production use please register a SMS provider in IdentityConfig and generate a code here. + var code = await UserManager.GenerateChangePhoneNumberTokenAsync(await GetCurrentUserAsync(), phoneNumber); + ViewBag.Status = "For DEMO purposes only, the current code is " + code; + return phoneNumber == null ? View("Error") : View(new VerifyPhoneNumberViewModel { PhoneNumber = phoneNumber }); + } + + // + // POST: /Account/VerifyPhoneNumber + [HttpPost] + [ValidateAntiForgeryToken] + public async Task VerifyPhoneNumber(VerifyPhoneNumberViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await UserManager.ChangePhoneNumberAsync(user, model.PhoneNumber, model.Code); + if (result.Succeeded) + { + await SignInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction("Index", new { Message = ManageMessageId.AddPhoneSuccess }); + } + } + // If we got this far, something failed, redisplay form + ModelState.AddModelError("", "Failed to verify phone"); + return View(model); + } + + // + // GET: /Account/RemovePhoneNumber + 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("Index", new { Message = ManageMessageId.RemovePhoneSuccess }); + } + } + return RedirectToAction("Index", new { Message = ManageMessageId.Error }); + } + + // + // GET: /Manage/ChangePassword + public IActionResult ChangePassword() + { + return View(); + } + + // + // POST: /Account/Manage + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ChangePassword(ChangePasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await UserManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); + if (result.Succeeded) + { + await SignInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction("Index", new { Message = ManageMessageId.ChangePasswordSuccess }); + } + AddErrors(result); + return View(model); + } + return RedirectToAction("Index", new { Message = ManageMessageId.Error }); + } + + // + // GET: /Manage/SetPassword + public IActionResult SetPassword() + { + return View(); + } + + // + // POST: /Manage/SetPassword + [HttpPost] + [ValidateAntiForgeryToken] + public async Task SetPassword(SetPasswordViewModel model) + { + if (!ModelState.IsValid) + { + 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("Index", new { Message = ManageMessageId.SetPasswordSuccess }); + } + AddErrors(result); + return View(model); + } + return RedirectToAction("Index", new { Message = ManageMessageId.Error }); + } + + // + // POST: /Manage/RememberBrowser + [HttpPost] + [ValidateAntiForgeryToken] + public async Task RememberBrowser() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + await SignInManager.RememberTwoFactorClient(user); + await SignInManager.SignInAsync(user, isPersistent: false); + } + return RedirectToAction("Index", "Manage"); + } + + // + // POST: /Manage/ForgetBrowser + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ForgetBrowser() + { + await SignInManager.ForgetTwoFactorClientAsync(); + return RedirectToAction("Index", "Manage"); + } + + // + // GET: /Account/Manage + //public async Task ManageLogins(ManageMessageId? message) + //{ + // ViewBag.StatusMessage = + // message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." + // : message == ManageMessageId.Error ? "An error has occurred." + // : ""; + // var user = await GetCurrentUserAsync(); + // if (user == null) + // { + // return View("Error"); + // } + // var userLogins = await UserManager.GetLoginsAsync(user); + //var otherLogins = AuthenticationManager.GetExternalAuthenticationTypes().Where(auth => userLogins.All(ul => auth.AuthenticationType != ul.LoginProvider)).ToList(); + //ViewBag.ShowRemoveButton = user.PasswordHash != null || userLogins.Count > 1; + //return View(new ManageLoginsViewModel + //{ + // CurrentLogins = userLogins, + // OtherLogins = otherLogins + //}); + //} + + #region Helpers + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError("", error); + } + } + + private async Task HasPhoneNumber() + { + var user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); + if (user != null) + { + return user.PhoneNumber != null; + } + return false; + } + + public enum ManageMessageId + { + AddPhoneSuccess, + ChangePasswordSuccess, + SetTwoFactorSuccess, + SetPasswordSuccess, + RemoveLoginSuccess, + RemovePhoneSuccess, + Error + } + + private async Task GetCurrentUserAsync() + { + return await UserManager.FindByIdAsync(Context.User.Identity.GetUserId()); + } + + private IActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + else + { + return RedirectToAction("Index", "Home"); + } + } + #endregion + } +} \ No newline at end of file diff --git a/src/MusicStore/LocalConfig.json b/src/MusicStore/LocalConfig.json index c4e905e13c..97c59012c5 100644 --- a/src/MusicStore/LocalConfig.json +++ b/src/MusicStore/LocalConfig.json @@ -1,5 +1,5 @@ { - "DefaultAdminUsername": "Administrator", + "DefaultAdminUsername": "Administrator@test.com", "DefaultAdminPassword": "YouShouldChangeThisPassword1!", "Data": { "DefaultConnection": { diff --git a/src/MusicStore/Models/AccountViewModels.cs b/src/MusicStore/Models/AccountViewModels.cs index 657a4f011d..248b510f0f 100644 --- a/src/MusicStore/Models/AccountViewModels.cs +++ b/src/MusicStore/Models/AccountViewModels.cs @@ -1,38 +1,58 @@ -using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Mvc.Rendering; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace MusicStore.Models { public class ExternalLoginConfirmationViewModel { [Required] - [Display(Name = "User name")] - public string UserName { get; set; } + [Display(Name = "Email")] + public string Email { get; set; } } - public class ManageUserViewModel + public class ExternalLoginListViewModel + { + public string ReturnUrl { get; set; } + } + + public class SendCodeViewModel + { + public string SelectedProvider { get; set; } + public ICollection Providers { get; set; } + public string ReturnUrl { get; set; } + public bool RememberMe { get; set; } + } + + public class VerifyCodeViewModel { [Required] - [DataType(DataType.Password)] - [Display(Name = "Current password")] - public string OldPassword { get; set; } + public string Provider { get; set; } [Required] - [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] - [DataType(DataType.Password)] - [Display(Name = "New password")] - public string NewPassword { get; set; } + [Display(Name = "Code")] + public string Code { get; set; } + public string ReturnUrl { get; set; } - [DataType(DataType.Password)] - [Display(Name = "Confirm new password")] - [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] - public string ConfirmPassword { get; set; } + [Display(Name = "Remember this browser?")] + public bool RememberBrowser { get; set; } + + public bool RememberMe { get; set; } + } + + public class ForgotViewModel + { + [Required] + [Display(Name = "Email")] + public string Email { get; set; } } public class LoginViewModel { [Required] - [Display(Name = "User name")] - public string UserName { get; set; } + [Display(Name = "Email")] + [EmailAddress] + public string Email { get; set; } [Required] [DataType(DataType.Password)] @@ -46,8 +66,9 @@ namespace MusicStore.Models public class RegisterViewModel { [Required] - [Display(Name = "User name")] - public string UserName { get; set; } + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] @@ -60,4 +81,33 @@ namespace MusicStore.Models [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } } + + public class ResetPasswordViewModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + + public string Code { get; set; } + } + + public class ForgotPasswordViewModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + } } \ No newline at end of file diff --git a/src/MusicStore/Models/ManageViewModels.cs b/src/MusicStore/Models/ManageViewModels.cs new file mode 100644 index 0000000000..e115fda993 --- /dev/null +++ b/src/MusicStore/Models/ManageViewModels.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Mvc.Rendering; + +namespace MusicStore.Models +{ + public class IndexViewModel + { + public bool HasPassword { get; set; } + public IList Logins { get; set; } + public string PhoneNumber { get; set; } + public bool TwoFactor { get; set; } + public bool BrowserRemembered { get; set; } + } + + public class ManageLoginsViewModel + { + public IList CurrentLogins { get; set; } + public IList OtherLogins { get; set; } + } + + public class FactorViewModel + { + public string Purpose { get; set; } + } + + public class SetPasswordViewModel + { + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public class ChangePasswordViewModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public class AddPhoneNumberViewModel + { + [Required] + [Phone] + [Display(Name = "Phone Number")] + public string Number { get; set; } + } + + public class VerifyPhoneNumberViewModel + { + [Required] + [Display(Name = "Code")] + public string Code { get; set; } + + [Required] + [Phone] + [Display(Name = "Phone Number")] + public string PhoneNumber { get; set; } + } + + public class ConfigureTwoFactorViewModel + { + public string SelectedProvider { get; set; } + public ICollection Providers { get; set; } + } +} \ No newline at end of file diff --git a/src/MusicStore/MusicStore.kproj b/src/MusicStore/MusicStore.kproj index c3e2a081bb..6c37376107 100644 --- a/src/MusicStore/MusicStore.kproj +++ b/src/MusicStore/MusicStore.kproj @@ -53,9 +53,7 @@ - - diff --git a/src/MusicStore/Startup.cs b/src/MusicStore/Startup.cs index 39c30f6add..56eb81dad3 100644 --- a/src/MusicStore/Startup.cs +++ b/src/MusicStore/Startup.cs @@ -24,9 +24,9 @@ namespace MusicStore configuration.AddJsonFile("LocalConfig.json"); configuration.AddEnvironmentVariables(); //All environment variables in the process's context flow in as configuration values. - /* Error page middleware displays a nice formatted HTML page for any unhandled exceptions in the request pipeline. - * Note: ErrorPageOptions.ShowAll to be used only at development time. Not recommended for production. - */ + /* Error page middleware displays a nice formatted HTML page for any unhandled exceptions in the request pipeline. + * Note: ErrorPageOptions.ShowAll to be used only at development time. Not recommended for production. + */ app.UseErrorPage(ErrorPageOptions.ShowAll); app.UseServices(services => @@ -78,9 +78,11 @@ namespace MusicStore app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = ClaimsIdentityOptions.DefaultAuthenticationType, - LoginPath = new PathString("/Account/Login"), + LoginPath = new PathString("/Account/Login") }); + app.UseTwoFactorSignInCookies(); + // Add MVC to the request pipeline app.UseMvc(routes => { diff --git a/src/MusicStore/Views/Account/ConfirmEmail.cshtml b/src/MusicStore/Views/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000000..98c66ede3a --- /dev/null +++ b/src/MusicStore/Views/Account/ConfirmEmail.cshtml @@ -0,0 +1,10 @@ +{ + ViewBag.Title = "Confirm Email"; +} + +

@ViewBag.Title.

+
+

+ Thank you for confirming your email. Please @Html.ActionLink("Click here to Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" }) +

+
\ No newline at end of file diff --git a/src/MusicStore/Views/Account/ExternalLoginConfirmation.cshtml b/src/MusicStore/Views/Account/ExternalLoginConfirmation.cshtml new file mode 100644 index 0000000000..c7811a5201 --- /dev/null +++ b/src/MusicStore/Views/Account/ExternalLoginConfirmation.cshtml @@ -0,0 +1,39 @@ +@model MusicStore.Models.ExternalLoginConfirmationViewModel +@{ + ViewBag.Title = "Register"; +} +

@ViewBag.Title.

+

Associate your @ViewBag.LoginProvider account.

+ +@using (Html.BeginForm("ExternalLoginConfirmation", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() + +

Association Form

+
+ @Html.ValidationSummary(true, "", new { @class = "text-danger" }) +

+ You've successfully authenticated with @ViewBag.LoginProvider. + Please enter a user name for this site below and click the Register button to finish + logging in. +

+
+ @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) +
+ @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) + @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" }) +
+
+
+
+ +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} \ No newline at end of file diff --git a/src/MusicStore/Views/Account/ExternalLoginFailure.cshtml b/src/MusicStore/Views/Account/ExternalLoginFailure.cshtml new file mode 100644 index 0000000000..3be4ab37d4 --- /dev/null +++ b/src/MusicStore/Views/Account/ExternalLoginFailure.cshtml @@ -0,0 +1,8 @@ +@{ + ViewBag.Title = "Login Failure"; +} + +
+

@ViewBag.Title.

+

Unsuccessful login with service.

+
diff --git a/src/MusicStore/Views/Account/ForgotPassword.cshtml b/src/MusicStore/Views/Account/ForgotPassword.cshtml new file mode 100644 index 0000000000..063998e4b4 --- /dev/null +++ b/src/MusicStore/Views/Account/ForgotPassword.cshtml @@ -0,0 +1,32 @@ +@model MusicStore.Models.ForgotPasswordViewModel +@{ + ViewBag.Title = "Forgot your password?"; +} + +

@ViewBag.Title.

+ +@using (Html.BeginForm("ForgotPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() +

Enter your email.

+
+ @Html.ValidationSummary("", new { @class = "text-danger" }) +
+ @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) +
+ @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) +
+
+
+
+ +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} diff --git a/src/MusicStore/Views/Account/ForgotPasswordConfirmation.cshtml b/src/MusicStore/Views/Account/ForgotPasswordConfirmation.cshtml new file mode 100644 index 0000000000..2ba9d0ca72 --- /dev/null +++ b/src/MusicStore/Views/Account/ForgotPasswordConfirmation.cshtml @@ -0,0 +1,15 @@ +@{ + ViewBag.Title = "Forgot Password Confirmation"; +} + +
+

@ViewBag.Title.

+
+
+

+ Please check your email to reset your password. +

+

+ For demo purpose only: @Html.ActionLink("Click here to reset the password: ", "ResetPassword", new { code = ViewBag.Code }); +

+
\ No newline at end of file diff --git a/src/MusicStore/Views/Account/Login.cshtml b/src/MusicStore/Views/Account/Login.cshtml index 0f5fa2c1ce..0b8cd41a86 100644 --- a/src/MusicStore/Views/Account/Login.cshtml +++ b/src/MusicStore/Views/Account/Login.cshtml @@ -1,4 +1,5 @@ -@model MusicStore.Models.LoginViewModel +@using MusicStore.Models +@model LoginViewModel @{ ViewBag.Title = "Log in"; @@ -13,19 +14,19 @@ @Html.AntiForgeryToken()

Use a local account to log in.


- @Html.ValidationSummary(true) + @Html.ValidationSummary(true, "", new { @class = "text-danger" })
- @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" }) + @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
- @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" }) - @Html.ValidationMessageFor(m => m.UserName) + @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) + @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
@Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
@Html.PasswordFor(m => m.Password, new { @class = "form-control" }) - @Html.ValidationMessageFor(m => m.Password) + @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
@@ -42,11 +43,20 @@

- @Html.ActionLink("Register", "Register") if you don't have a local account. + @Html.ActionLink("Register as a new user", "Register") +

+ @*Enable this once you have account confirmation enabled for password reset functionality*@ +

+ @Html.ActionLink("Forgot your password?", "ForgotPassword")

} + @*
+
+ @Html.PartialAsync("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl }) +
+
*@ @section Scripts { @*TODO : Until script helpers are available, adding script references manually*@ diff --git a/src/MusicStore/Views/Account/Manage.cshtml b/src/MusicStore/Views/Account/Manage.cshtml deleted file mode 100644 index 714decf75b..0000000000 --- a/src/MusicStore/Views/Account/Manage.cshtml +++ /dev/null @@ -1,19 +0,0 @@ -@{ - ViewBag.Title = "Manage Account"; -} - -

@ViewBag.Title.

-

@ViewBag.StatusMessage

- -
-
- @await Html.PartialAsync("_ChangePasswordPartial") -
-
- -@section Scripts { - @*TODO : Until script helpers are available, adding script references manually*@ - @*@Scripts.Render("~/bundles/jqueryval")*@ - - -} \ No newline at end of file diff --git a/src/MusicStore/Views/Account/Register.cshtml b/src/MusicStore/Views/Account/Register.cshtml index 7eb99e5b56..4f713e0756 100644 --- a/src/MusicStore/Views/Account/Register.cshtml +++ b/src/MusicStore/Views/Account/Register.cshtml @@ -10,11 +10,11 @@ @Html.AntiForgeryToken()

Create a new account.


- @Html.ValidationSummary() + @Html.ValidationSummary("", new { @class = "text-danger" })
- @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" }) + @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
- @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" }) + @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
diff --git a/src/MusicStore/Views/Account/RegisterConfirmation.cshtml b/src/MusicStore/Views/Account/RegisterConfirmation.cshtml new file mode 100644 index 0000000000..549f6ef105 --- /dev/null +++ b/src/MusicStore/Views/Account/RegisterConfirmation.cshtml @@ -0,0 +1,15 @@ +@{ + ViewBag.Title = "Register Confirmation"; +} + +
+

@ViewBag.Title.

+
+
+

+ Please check your email to activate your account. +

+

+ Demo/testing purposes only: The sample displays the code and user id in the page: @Html.ActionLink("Click here to confirm your email: ", "ConfirmEmail", new { code = ViewBag.Code, userId = ViewBag.UserId }) +

+
\ No newline at end of file diff --git a/src/MusicStore/Views/Account/ResetPassword.cshtml b/src/MusicStore/Views/Account/ResetPassword.cshtml new file mode 100644 index 0000000000..7ee7e8276a --- /dev/null +++ b/src/MusicStore/Views/Account/ResetPassword.cshtml @@ -0,0 +1,45 @@ +@model MusicStore.Models.ResetPasswordViewModel +@{ + ViewBag.Title = "Reset password"; +} + +

@ViewBag.Title.

+ +@using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() +

Reset your password.

+
+ @Html.ValidationSummary("", new { @class = "text-danger" }) + @Html.HiddenFor(model => model.Code) +
+ @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) +
+ @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) +
+
+
+ @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) +
+ @Html.PasswordFor(m => m.Password, new { @class = "form-control" }) +
+
+
+ @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) +
+ @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) +
+
+
+
+ +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} \ No newline at end of file diff --git a/src/MusicStore/Views/Account/ResetPasswordConfirmation.cshtml b/src/MusicStore/Views/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 0000000000..3804516176 --- /dev/null +++ b/src/MusicStore/Views/Account/ResetPasswordConfirmation.cshtml @@ -0,0 +1,12 @@ +@{ + ViewBag.Title = "Reset password confirmation"; +} + +
+

@ViewBag.Title.

+
+
+

+ Your password has been reset. Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" }) +

+
diff --git a/src/MusicStore/Views/Account/SendCode.cshtml b/src/MusicStore/Views/Account/SendCode.cshtml new file mode 100644 index 0000000000..aa3395fa9d --- /dev/null +++ b/src/MusicStore/Views/Account/SendCode.cshtml @@ -0,0 +1,27 @@ +@model MusicStore.Models.SendCodeViewModel +@{ + ViewBag.Title = "Send"; +} + +

@ViewBag.Title.

+ +@using (Html.BeginForm("SendCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { + @Html.AntiForgeryToken() + @Html.Hidden("rememberMe", @Model.RememberMe) +

Send verification code

+
+
+
+ Select Two-Factor Authentication Provider: + @Html.DropDownListFor(model => model.SelectedProvider, Model.Providers) + +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} \ No newline at end of file diff --git a/src/MusicStore/Views/Account/_ExternalLoginsListPartial.cshtml b/src/MusicStore/Views/Account/_ExternalLoginsListPartial.cshtml new file mode 100644 index 0000000000..b47662f1c9 --- /dev/null +++ b/src/MusicStore/Views/Account/_ExternalLoginsListPartial.cshtml @@ -0,0 +1,28 @@ +@model MusicStore.Models.ExternalLoginListViewModel +@using Microsoft.AspNet.Http.Security; + +

Use another service to log in.

+
+@{ + var loginProviders = Context.GetAuthenticationTypes(); + if (loginProviders.Count() == 0) { +
+

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

+
+ } + else { + using (Html.BeginForm("ExternalLogin", "Account", new { ReturnUrl = Model.ReturnUrl })) { + @Html.AntiForgeryToken() +
+

+ @foreach (AuthenticationDescription p in loginProviders) { + + } +

+
+ } + } +} \ No newline at end of file diff --git a/src/MusicStore/Views/Manage/AddPhoneNumber.cshtml b/src/MusicStore/Views/Manage/AddPhoneNumber.cshtml new file mode 100644 index 0000000000..2252e07656 --- /dev/null +++ b/src/MusicStore/Views/Manage/AddPhoneNumber.cshtml @@ -0,0 +1,32 @@ +@model MusicStore.Models.AddPhoneNumberViewModel +@{ + ViewBag.Title = "Phone Number"; +} + +

@ViewBag.Title.

+ +@using (Html.BeginForm("AddPhoneNumber", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() +

Add a phone number

+
+ @Html.ValidationSummary("", new { @class = "text-danger" }) +
+ @Html.LabelFor(m => m.Number, new { @class = "col-md-2 control-label" }) +
+ @Html.TextBoxFor(m => m.Number, new { @class = "form-control" }) +
+
+
+
+ +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} \ No newline at end of file diff --git a/src/MusicStore/Views/Account/_ChangePasswordPartial.cshtml b/src/MusicStore/Views/Manage/ChangePassword.cshtml similarity index 62% rename from src/MusicStore/Views/Account/_ChangePasswordPartial.cshtml rename to src/MusicStore/Views/Manage/ChangePassword.cshtml index 7c84c7e291..04edda2e27 100644 --- a/src/MusicStore/Views/Account/_ChangePasswordPartial.cshtml +++ b/src/MusicStore/Views/Manage/ChangePassword.cshtml @@ -1,14 +1,16 @@ -@using System.Security.Principal -@model MusicStore.Models.ManageUserViewModel +@model MusicStore.Models.ChangePasswordViewModel +@{ + ViewBag.Title = "Change Password"; +} -

You're logged in as @User.Identity.GetUserName().

+

@ViewBag.Title.

-@using (Html.BeginForm("Manage", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +@using (Html.BeginForm("ChangePassword", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { @Html.AntiForgeryToken()

Change Password Form


- @Html.ValidationSummary() + @Html.ValidationSummary("", new { @class = "text-danger" })
@Html.LabelFor(m => m.OldPassword, new { @class = "col-md-2 control-label" })
@@ -27,10 +29,15 @@ @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
-
+} +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + } \ No newline at end of file diff --git a/src/MusicStore/Views/Manage/Index.cshtml b/src/MusicStore/Views/Manage/Index.cshtml new file mode 100644 index 0000000000..0b78ee2fd9 --- /dev/null +++ b/src/MusicStore/Views/Manage/Index.cshtml @@ -0,0 +1,106 @@ +@model MusicStore.Models.IndexViewModel +@{ + ViewBag.Title = "Manage"; +} + +

@ViewBag.Title.

+ +

@ViewBag.StatusMessage

+
+

Change your account settings

+
+
+
Password:
+
+ [ + @if (Model.HasPassword) + { + @Html.ActionLink("Change your password", "ChangePassword") + } + else + { + @Html.ActionLink("Create", "SetPassword") + } + ] +
+
External Logins:
+
+ @Model.Logins.Count [ + @Html.ActionLink("Manage", "ManageLogins") ] +
+ @* + Phone Numbers can used as a second factor of verification in a two-factor authentication system. + + See this article + for details on setting up this ASP.NET application to support two-factor authentication using SMS. + + Uncomment the following block after you have set up two-factor authentication + *@ + +
Phone Number:
+
+ @(Model.PhoneNumber ?? "None") [ + @if (Model.PhoneNumber != null) + { + @Html.ActionLink("Change", "AddPhoneNumber") + @:  |  + @Html.ActionLink("Remove", "RemovePhoneNumber") + } + else + { + @Html.ActionLink("Add", "AddPhoneNumber") + } + ] +
+
Two-Factor Authentication:
+
+ @if (Model.TwoFactor) + { + using (Html.BeginForm("DisableTwoFactorAuthentication", "Manage", FormMethod.Post)) + { + @Html.AntiForgeryToken() +

+ Enabled + +

+ } + } + else + { + using (Html.BeginForm("EnableTwoFactorAuthentication", "Manage", FormMethod.Post)) + { + @Html.AntiForgeryToken() +

+ Disabled + +

+ } + } +
+
Browser remembered:
+
+ @if (Model.BrowserRemembered) + { + using (Html.BeginForm("ForgetBrowser", "Manage", FormMethod.Post)) + { + @Html.AntiForgeryToken() +

+ Browser is curently remembered for two factor: + +

+ } + } + else + { + using (Html.BeginForm("RememberBrowser", "Manage", FormMethod.Post)) + { + @Html.AntiForgeryToken() +

+ Browser is curently not remembered for two factor: + +

+ } + } +
+
+
diff --git a/src/MusicStore/Views/Manage/ManageLogins.cshtml b/src/MusicStore/Views/Manage/ManageLogins.cshtml new file mode 100644 index 0000000000..94cc9b454b --- /dev/null +++ b/src/MusicStore/Views/Manage/ManageLogins.cshtml @@ -0,0 +1,58 @@ +@using Microsoft.AspNet.Http.Security; +@model MusicStore.Models.ManageLoginsViewModel +@{ + ViewBag.Title = "Manage your external logins"; +} + +

@ViewBag.Title.

+ +

@ViewBag.StatusMessage

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

Registered Logins

+ + + @foreach (var account in Model.CurrentLogins) + { + + + + + } + +
@account.LoginProvider + @if (ViewBag.ShowRemoveButton) + { + using (Html.BeginForm("RemoveLogin", "Manage")) + { + @Html.AntiForgeryToken() +
+ @Html.Hidden("loginProvider", account.LoginProvider) + @Html.Hidden("providerKey", account.ProviderKey) + +
+ } + } + else + { + @:   + } +
+} +@if (Model.OtherLogins.Count > 0) +{ +

Add another service to log in.

+
+ using (Html.BeginForm("LinkLogin", "Manage")) + { + @Html.AntiForgeryToken() +
+

+ @foreach (AuthenticationDescription p in Model.OtherLogins) + { + + } +

+
+ } +} \ No newline at end of file diff --git a/src/MusicStore/Views/Manage/SetPassword.cshtml b/src/MusicStore/Views/Manage/SetPassword.cshtml new file mode 100644 index 0000000000..cb9824586d --- /dev/null +++ b/src/MusicStore/Views/Manage/SetPassword.cshtml @@ -0,0 +1,43 @@ +@model MusicStore.Models.SetPasswordViewModel +@{ + ViewBag.Title = "Create Password"; +} + +

@ViewBag.Title.

+

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

+ +@using (Html.BeginForm("SetPassword", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() + +

Create Local Login

+
+ @Html.ValidationSummary("", new { @class = "text-danger" }) +
+ @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" }) +
+ @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" }) +
+
+
+ @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) +
+ @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) +
+
+
+
+ +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} \ No newline at end of file diff --git a/src/MusicStore/Views/Manage/VerifyPhoneNumber.cshtml b/src/MusicStore/Views/Manage/VerifyPhoneNumber.cshtml new file mode 100644 index 0000000000..6ab6abe5cc --- /dev/null +++ b/src/MusicStore/Views/Manage/VerifyPhoneNumber.cshtml @@ -0,0 +1,34 @@ +@model MusicStore.Models.VerifyPhoneNumberViewModel +@{ + ViewBag.Title = "Verify Phone Number"; +} + +

@ViewBag.Title.

+ +@using (Html.BeginForm("VerifyPhoneNumber", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() + @Html.Hidden("phoneNumber", @Model.PhoneNumber) +

Enter verification code

+
@ViewBag.Status
+
+ @Html.ValidationSummary("", new { @class = "text-danger" }) +
+ @Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" }) +
+ @Html.TextBoxFor(m => m.Code, new { @class = "form-control" }) +
+
+
+
+ +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} \ No newline at end of file diff --git a/src/MusicStore/Views/Shared/DemoCodeDisplay.cshtml b/src/MusicStore/Views/Shared/DemoCodeDisplay.cshtml new file mode 100644 index 0000000000..1b188da8c5 --- /dev/null +++ b/src/MusicStore/Views/Shared/DemoCodeDisplay.cshtml @@ -0,0 +1,15 @@ +@{ + ViewBag.Title = "Demo code display page - Not for production use"; +} + +
+

@ViewBag.Title.

+
+
+

+ Demo code display page - Not for production use. +

+

+ [Demo]: @ViewBag.Status +

+
\ No newline at end of file diff --git a/src/MusicStore/Views/Shared/Lockout.cshtml b/src/MusicStore/Views/Shared/Lockout.cshtml new file mode 100644 index 0000000000..aff107a063 --- /dev/null +++ b/src/MusicStore/Views/Shared/Lockout.cshtml @@ -0,0 +1,8 @@ +@{ + ViewBag.Title = "Locked Out"; +} + +
+

Locked out.

+

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

+
\ No newline at end of file diff --git a/src/MusicStore/Views/Shared/_LoginPartial.cshtml b/src/MusicStore/Views/Shared/_LoginPartial.cshtml index d8368a8a7d..5da720b97f 100644 --- a/src/MusicStore/Views/Shared/_LoginPartial.cshtml +++ b/src/MusicStore/Views/Shared/_LoginPartial.cshtml @@ -8,7 +8,7 @@ diff --git a/test/E2ETests/SmokeTests.cs b/test/E2ETests/SmokeTests.cs index 784fbd1f8b..2e1015f421 100644 --- a/test/E2ETests/SmokeTests.cs +++ b/test/E2ETests/SmokeTests.cs @@ -97,36 +97,36 @@ namespace E2ETests RegisterUserWithNonMatchingPasswords(); //Register a valid user - var generatedUserName = RegisterValidUser(); + var generatedEmail = RegisterValidUser(); //Register a user - Negative scenario : Trying to register a user name that's already registered. - RegisterExistingUser(generatedUserName); + RegisterExistingUser(generatedEmail); //Logout from this user session - This should take back to the home page - SignOutUser(generatedUserName); + SignOutUser(generatedEmail); //Sign in scenarios: Invalid password - Expected an invalid user name password error. - SignInWithInvalidPassword(generatedUserName, "InvalidPassword~1"); + SignInWithInvalidPassword(generatedEmail, "InvalidPassword~1"); //Sign in scenarios: Valid user name & password. - SignInWithUser(generatedUserName, "Password~1"); + SignInWithUser(generatedEmail, "Password~1"); //Change password scenario - ChangePassword(generatedUserName); + ChangePassword(generatedEmail); //SignIn with old password and verify old password is not allowed and new password is allowed - SignOutUser(generatedUserName); - SignInWithInvalidPassword(generatedUserName, "Password~1"); - SignInWithUser(generatedUserName, "Password~2"); + SignOutUser(generatedEmail); + SignInWithInvalidPassword(generatedEmail, "Password~1"); + SignInWithUser(generatedEmail, "Password~2"); //Making a request to a protected resource that this user does not have access to - should automatically redirect to login page again - AccessStoreWithoutPermissions(generatedUserName); + AccessStoreWithoutPermissions(generatedEmail); //Logout from this user session - This should take back to the home page - SignOutUser(generatedUserName); + SignOutUser(generatedEmail); //Login as an admin user - SignInWithUser("Administrator", "YouShouldChangeThisPassword1!"); + SignInWithUser("Administrator@test.com", "YouShouldChangeThisPassword1!"); //Now navigating to the store manager should work fine as this user has the necessary permission to administer the store. AccessStoreWithPermissions(); @@ -222,9 +222,9 @@ namespace E2ETests Assert.Contains("
  • ", responseContent, StringComparison.OrdinalIgnoreCase); } - private void AccessStoreWithoutPermissions(string userName = null) + private void AccessStoreWithoutPermissions(string email = null) { - Console.WriteLine("Trying to access StoreManager that needs ManageStore claim with the current user : {0}", userName ?? "Anonymous"); + Console.WriteLine("Trying to access StoreManager that needs ManageStore claim with the current user : {0}", email ?? "Anonymous"); var response = httpClient.GetAsync("StoreManager/").Result; ThrowIfResponseStatusNotOk(response); var responseContent = response.Content.ReadAsStringAsync().Result; @@ -259,11 +259,11 @@ namespace E2ETests var responseContent = response.Content.ReadAsStringAsync().Result; ValidateLayoutPage(responseContent); - var generatedUserName = Guid.NewGuid().ToString().Replace("-", string.Empty); - Console.WriteLine("Creating a new user with name '{0}'", generatedUserName); + var generatedEmail = Guid.NewGuid().ToString().Replace("-", string.Empty) + "@test.com"; + Console.WriteLine("Creating a new user with name '{0}'", generatedEmail); var formParameters = new List> { - new KeyValuePair("UserName", generatedUserName), + new KeyValuePair("Email", generatedEmail), new KeyValuePair("Password", "Password~1"), new KeyValuePair("ConfirmPassword", "Password~2"), new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/Register")), @@ -273,8 +273,8 @@ namespace E2ETests response = httpClient.PostAsync("Account/Register", content).Result; responseContent = response.Content.ReadAsStringAsync().Result; Assert.Null(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl)).GetCookieWithName(".AspNet.Microsoft.AspNet.Identity.Application")); - Assert.Contains("
    • The password and confirmation password do not match.
    • ", responseContent, StringComparison.OrdinalIgnoreCase); - Console.WriteLine("Server side model validator rejected the user '{0}''s registration as passwords do not match.", generatedUserName); + Assert.Contains("
      • The password and confirmation password do not match.
      • ", responseContent, StringComparison.OrdinalIgnoreCase); + Console.WriteLine("Server side model validator rejected the user '{0}''s registration as passwords do not match.", generatedEmail); } private string RegisterValidUser() @@ -284,11 +284,11 @@ namespace E2ETests var responseContent = response.Content.ReadAsStringAsync().Result; ValidateLayoutPage(responseContent); - var generatedUserName = Guid.NewGuid().ToString().Replace("-", string.Empty); - Console.WriteLine("Creating a new user with name '{0}'", generatedUserName); + var generatedEmail = Guid.NewGuid().ToString().Replace("-", string.Empty) + "@test.com"; + Console.WriteLine("Creating a new user with name '{0}'", generatedEmail); var formParameters = new List> { - new KeyValuePair("UserName", generatedUserName), + new KeyValuePair("Email", generatedEmail), new KeyValuePair("Password", "Password~1"), new KeyValuePair("ConfirmPassword", "Password~1"), new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/Register")), @@ -297,24 +297,24 @@ namespace E2ETests var content = new FormUrlEncodedContent(formParameters.ToArray()); response = httpClient.PostAsync("Account/Register", content).Result; responseContent = response.Content.ReadAsStringAsync().Result; - Assert.Contains(string.Format("Hello {0}!", generatedUserName), responseContent, StringComparison.OrdinalIgnoreCase); + Assert.Contains(string.Format("Hello {0}!", generatedEmail), responseContent, StringComparison.OrdinalIgnoreCase); Assert.Contains("Log off", responseContent, StringComparison.OrdinalIgnoreCase); //Verify cookie sent Assert.NotNull(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl)).GetCookieWithName(".AspNet.Microsoft.AspNet.Identity.Application")); - Console.WriteLine("Successfully registered user '{0}' and signed in", generatedUserName); - return generatedUserName; + Console.WriteLine("Successfully registered user '{0}' and signed in", generatedEmail); + return generatedEmail; } - private void RegisterExistingUser(string userName) + private void RegisterExistingUser(string email) { - Console.WriteLine("Trying to register a user with name '{0}' again", userName); + Console.WriteLine("Trying to register a user with name '{0}' again", email); var response = httpClient.GetAsync("Account/Register").Result; ThrowIfResponseStatusNotOk(response); var responseContent = response.Content.ReadAsStringAsync().Result; - Console.WriteLine("Creating a new user with name '{0}'", userName); + Console.WriteLine("Creating a new user with name '{0}'", email); var formParameters = new List> { - new KeyValuePair("UserName", userName), + new KeyValuePair("Email", email), new KeyValuePair("Password", "Password~1"), new KeyValuePair("ConfirmPassword", "Password~1"), new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/Register")), @@ -323,13 +323,13 @@ namespace E2ETests var content = new FormUrlEncodedContent(formParameters.ToArray()); response = httpClient.PostAsync("Account/Register", content).Result; responseContent = response.Content.ReadAsStringAsync().Result; - Assert.Contains(string.Format("Name {0} is already taken.", userName), responseContent, StringComparison.OrdinalIgnoreCase); - Console.WriteLine("Identity threw a valid exception that user '{0}' already exists in the system", userName); + Assert.Contains(string.Format("Name {0} is already taken.", email), responseContent, StringComparison.OrdinalIgnoreCase); + Console.WriteLine("Identity threw a valid exception that user '{0}' already exists in the system", email); } - private void SignOutUser(string userName) + private void SignOutUser(string email) { - Console.WriteLine("Signing out from '{0}''s session", userName); + Console.WriteLine("Signing out from '{0}''s session", email); var response = httpClient.GetAsync(string.Empty).Result; ThrowIfResponseStatusNotOk(response); var responseContent = response.Content.ReadAsStringAsync().Result; @@ -352,7 +352,7 @@ namespace E2ETests Assert.Contains("/Images/home-showcase.png", responseContent, StringComparison.OrdinalIgnoreCase); //Verify cookie cleared on logout Assert.Null(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl)).GetCookieWithName(".AspNet.Microsoft.AspNet.Identity.Application")); - Console.WriteLine("Successfully signed out of '{0}''s session", userName); + Console.WriteLine("Successfully signed out of '{0}''s session", email); } else { @@ -362,15 +362,15 @@ namespace E2ETests } } - private void SignInWithInvalidPassword(string userName, string invalidPassword) + private void SignInWithInvalidPassword(string email, string invalidPassword) { var response = httpClient.GetAsync("Account/Login").Result; ThrowIfResponseStatusNotOk(response); var responseContent = response.Content.ReadAsStringAsync().Result; - Console.WriteLine("Signing in with user '{0}'", userName); + Console.WriteLine("Signing in with user '{0}'", email); var formParameters = new List> { - new KeyValuePair("UserName", userName), + new KeyValuePair("Email", email), new KeyValuePair("Password", invalidPassword), new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/Login")), }; @@ -378,21 +378,21 @@ namespace E2ETests var content = new FormUrlEncodedContent(formParameters.ToArray()); response = httpClient.PostAsync("Account/Login", content).Result; responseContent = response.Content.ReadAsStringAsync().Result; - Assert.Contains("
        • Invalid username or password.
        • ", responseContent, StringComparison.OrdinalIgnoreCase); + Assert.Contains("
          • Invalid login attempt.
          • ", responseContent, StringComparison.OrdinalIgnoreCase); //Verify cookie not sent Assert.Null(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl)).GetCookieWithName(".AspNet.Microsoft.AspNet.Identity.Application")); Console.WriteLine("Identity successfully prevented an invalid user login."); } - private void SignInWithUser(string userName, string password) + private void SignInWithUser(string email, string password) { var response = httpClient.GetAsync("Account/Login").Result; ThrowIfResponseStatusNotOk(response); var responseContent = response.Content.ReadAsStringAsync().Result; - Console.WriteLine("Signing in with user '{0}'", userName); + Console.WriteLine("Signing in with user '{0}'", email); var formParameters = new List> { - new KeyValuePair("UserName", userName), + new KeyValuePair("Email", email), new KeyValuePair("Password", password), new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/Login")), }; @@ -400,16 +400,16 @@ namespace E2ETests var content = new FormUrlEncodedContent(formParameters.ToArray()); response = httpClient.PostAsync("Account/Login", content).Result; responseContent = response.Content.ReadAsStringAsync().Result; - Assert.Contains(string.Format("Hello {0}!", userName), responseContent, StringComparison.OrdinalIgnoreCase); + Assert.Contains(string.Format("Hello {0}!", email), responseContent, StringComparison.OrdinalIgnoreCase); Assert.Contains("Log off", responseContent, StringComparison.OrdinalIgnoreCase); //Verify cookie sent Assert.NotNull(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl)).GetCookieWithName(".AspNet.Microsoft.AspNet.Identity.Application")); - Console.WriteLine("Successfully signed in with user '{0}'", userName); + Console.WriteLine("Successfully signed in with user '{0}'", email); } - private void ChangePassword(string userName) + private void ChangePassword(string email) { - var response = httpClient.GetAsync("Account/Manage").Result; + var response = httpClient.GetAsync("Manage/ChangePassword").Result; ThrowIfResponseStatusNotOk(response); var responseContent = response.Content.ReadAsStringAsync().Result; var formParameters = new List> @@ -417,15 +417,15 @@ namespace E2ETests new KeyValuePair("OldPassword", "Password~1"), new KeyValuePair("NewPassword", "Password~2"), new KeyValuePair("ConfirmPassword", "Password~2"), - new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/Manage")), + new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Manage/ChangePassword")), }; var content = new FormUrlEncodedContent(formParameters.ToArray()); - response = httpClient.PostAsync("Account/Manage", content).Result; + response = httpClient.PostAsync("Manage/ChangePassword", content).Result; responseContent = response.Content.ReadAsStringAsync().Result; Assert.Contains("Your password has been changed.", responseContent, StringComparison.OrdinalIgnoreCase); Assert.NotNull(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl)).GetCookieWithName(".AspNet.Microsoft.AspNet.Identity.Application")); - Console.WriteLine("Successfully changed the password for user '{0}'", userName); + Console.WriteLine("Successfully changed the password for user '{0}'", email); } private string CreateAlbum()