Browse Source
* Adding support for 2fa, more auth options * WIP getting auth stuff working * Added Manage views. 2FA working now for MVC app. * Switching to using a controller for no-UI logout scenario * Adding Razor Pages impl of 2FA auth stuff. Works.main
committed by
GitHub
75 changed files with 2702 additions and 58 deletions
@ -0,0 +1,10 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace ApplicationCore.Interfaces |
||||
|
{ |
||||
|
|
||||
|
public interface IEmailSender |
||||
|
{ |
||||
|
Task SendEmailAsync(string email, string subject, string message); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using ApplicationCore.Interfaces; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Infrastructure.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) |
||||
|
{ |
||||
|
// TODO: Wire this up to actual email sending logic via SendGrid, local SMTP, etc.
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,499 @@ |
|||||
|
using ApplicationCore.Interfaces; |
||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Authentication; |
||||
|
using Microsoft.AspNetCore.Authorization; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.eShopWeb.ViewModels.Manage; |
||||
|
using Microsoft.eShopWeb.Services; |
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Text; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.Controllers |
||||
|
{ |
||||
|
[Authorize] |
||||
|
[Route("[controller]/[action]")]
|
||||
|
public class ManageController : Controller |
||||
|
{ |
||||
|
private readonly UserManager<ApplicationUser> _userManager; |
||||
|
private readonly SignInManager<ApplicationUser> _signInManager; |
||||
|
private readonly IEmailSender _emailSender; |
||||
|
private readonly IAppLogger<ManageController> _logger; |
||||
|
private readonly UrlEncoder _urlEncoder; |
||||
|
|
||||
|
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; |
||||
|
|
||||
|
public ManageController( |
||||
|
UserManager<ApplicationUser> userManager, |
||||
|
SignInManager<ApplicationUser> signInManager, |
||||
|
IEmailSender emailSender, |
||||
|
IAppLogger<ManageController> logger, |
||||
|
UrlEncoder urlEncoder) |
||||
|
{ |
||||
|
_userManager = userManager; |
||||
|
_signInManager = signInManager; |
||||
|
_emailSender = emailSender; |
||||
|
_logger = logger; |
||||
|
_urlEncoder = urlEncoder; |
||||
|
} |
||||
|
|
||||
|
[TempData] |
||||
|
public string StatusMessage { get; set; } |
||||
|
|
||||
|
[HttpGet] |
||||
|
public async Task<IActionResult> Index() |
||||
|
{ |
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
var model = new IndexViewModel |
||||
|
{ |
||||
|
Username = user.UserName, |
||||
|
Email = user.Email, |
||||
|
PhoneNumber = user.PhoneNumber, |
||||
|
IsEmailConfirmed = user.EmailConfirmed, |
||||
|
StatusMessage = StatusMessage |
||||
|
}; |
||||
|
|
||||
|
return View(model); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[ValidateAntiForgeryToken] |
||||
|
public async Task<IActionResult> Index(IndexViewModel 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)}'."); |
||||
|
} |
||||
|
|
||||
|
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)); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[ValidateAntiForgeryToken] |
||||
|
public async Task<IActionResult> SendVerificationEmail(IndexViewModel 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)}'."); |
||||
|
} |
||||
|
|
||||
|
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<IActionResult> 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); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[ValidateAntiForgeryToken] |
||||
|
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel 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)}'."); |
||||
|
} |
||||
|
|
||||
|
var changePasswordResult = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); |
||||
|
if (!changePasswordResult.Succeeded) |
||||
|
{ |
||||
|
AddErrors(changePasswordResult); |
||||
|
return View(model); |
||||
|
} |
||||
|
|
||||
|
await _signInManager.SignInAsync(user, isPersistent: false); |
||||
|
_logger.LogInformation("User changed their password successfully."); |
||||
|
StatusMessage = "Your password has been changed."; |
||||
|
|
||||
|
return RedirectToAction(nameof(ChangePassword)); |
||||
|
} |
||||
|
|
||||
|
[HttpGet] |
||||
|
public async Task<IActionResult> SetPassword() |
||||
|
{ |
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[ValidateAntiForgeryToken] |
||||
|
public async Task<IActionResult> SetPassword(SetPasswordViewModel 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)}'."); |
||||
|
} |
||||
|
|
||||
|
var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword); |
||||
|
if (!addPasswordResult.Succeeded) |
||||
|
{ |
||||
|
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<IActionResult> 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); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[ValidateAntiForgeryToken] |
||||
|
public async Task<IActionResult> LinkLogin(string provider) |
||||
|
{ |
||||
|
// Clear the existing external cookie to ensure a clean login process
|
||||
|
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)); |
||||
|
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); |
||||
|
return new ChallengeResult(provider, properties); |
||||
|
} |
||||
|
|
||||
|
[HttpGet] |
||||
|
public async Task<IActionResult> LinkLoginCallback() |
||||
|
{ |
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
var info = await _signInManager.GetExternalLoginInfoAsync(user.Id); |
||||
|
if (info == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'."); |
||||
|
} |
||||
|
|
||||
|
var result = await _userManager.AddLoginAsync(user, info); |
||||
|
if (!result.Succeeded) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'."); |
||||
|
} |
||||
|
|
||||
|
// 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<IActionResult> 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<IActionResult> 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.TwoFactorEnabled, |
||||
|
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user), |
||||
|
}; |
||||
|
|
||||
|
return View(model); |
||||
|
} |
||||
|
|
||||
|
[HttpGet] |
||||
|
public async Task<IActionResult> Disable2faWarning() |
||||
|
{ |
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
if (!user.TwoFactorEnabled) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'."); |
||||
|
} |
||||
|
|
||||
|
return View(nameof(Disable2fa)); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[ValidateAntiForgeryToken] |
||||
|
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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.TwoFactorCode", "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<IActionResult> 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<IActionResult> GenerateRecoveryCodes() |
||||
|
{ |
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
if (!user.TwoFactorEnabled) |
||||
|
{ |
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
private void AddErrors(IdentityResult result) |
||||
|
{ |
||||
|
foreach (var error in result.Errors) |
||||
|
{ |
||||
|
ModelState.AddModelError(string.Empty, error.Description); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private string FormatKey(string unformattedKey) |
||||
|
{ |
||||
|
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 string GenerateQrCodeUri(string email, string unformattedKey) |
||||
|
{ |
||||
|
return string.Format( |
||||
|
AuthenicatorUriFormat, |
||||
|
_urlEncoder.Encode("eShopOnWeb"), |
||||
|
_urlEncoder.Encode(email), |
||||
|
unformattedKey); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
using ApplicationCore.Interfaces; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.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: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
using Microsoft.eShopWeb.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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,6 +1,6 @@ |
|||||
using System.ComponentModel.DataAnnotations; |
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
namespace Microsoft.eShopWeb.ViewModels |
namespace Microsoft.eShopWeb.ViewModels.Account |
||||
{ |
{ |
||||
public class LoginViewModel |
public class LoginViewModel |
||||
{ |
{ |
||||
@ -0,0 +1,18 @@ |
|||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ViewModels.Account |
||||
|
{ |
||||
|
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; } |
||||
|
} |
||||
|
} |
||||
@ -1,7 +1,7 @@ |
|||||
using System; |
using System; |
||||
using System.ComponentModel.DataAnnotations; |
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
namespace Microsoft.eShopWeb.ViewModels |
namespace Microsoft.eShopWeb.ViewModels.Account |
||||
{ |
{ |
||||
public class RegisterViewModel |
public class RegisterViewModel |
||||
{ |
{ |
||||
@ -0,0 +1,23 @@ |
|||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace Web.ViewModels.Account |
||||
|
{ |
||||
|
public class ResetPasswordViewModel |
||||
|
{ |
||||
|
[Required] |
||||
|
[EmailAddress] |
||||
|
public string Email { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] |
||||
|
[DataType(DataType.Password)] |
||||
|
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; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ViewModels.Manage |
||||
|
{ |
||||
|
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} and at max {1} 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 string StatusMessage { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System.ComponentModel; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ViewModels.Manage |
||||
|
{ |
||||
|
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; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using Microsoft.AspNetCore.Authentication; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ViewModels.Manage |
||||
|
{ |
||||
|
public class ExternalLoginsViewModel |
||||
|
{ |
||||
|
public IList<UserLoginInfo> CurrentLogins { get; set; } |
||||
|
public IList<AuthenticationScheme> OtherLogins { get; set; } |
||||
|
public bool ShowRemoveButton { get; set; } |
||||
|
public string StatusMessage { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace Microsoft.eShopWeb.ViewModels.Manage |
||||
|
{ |
||||
|
public class GenerateRecoveryCodesViewModel |
||||
|
{ |
||||
|
public string[] RecoveryCodes { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ViewModels.Manage |
||||
|
{ |
||||
|
public class IndexViewModel |
||||
|
{ |
||||
|
public string Username { 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 string StatusMessage { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
namespace Microsoft.eShopWeb.ViewModels.Manage |
||||
|
{ |
||||
|
public class RemoveLoginViewModel |
||||
|
{ |
||||
|
public string LoginProvider { get; set; } |
||||
|
public string ProviderKey { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ViewModels.Manage |
||||
|
{ |
||||
|
public class SetPasswordViewModel |
||||
|
{ |
||||
|
[Required] |
||||
|
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} 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 string StatusMessage { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ViewModels.Manage |
||||
|
{ |
||||
|
public class TwoFactorAuthenticationViewModel |
||||
|
{ |
||||
|
public bool HasAuthenticator { get; set; } |
||||
|
public int RecoveryCodesLeft { get; set; } |
||||
|
public bool Is2faEnabled { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
@{ |
||||
|
ViewData["Title"] = "Locked out"; |
||||
|
} |
||||
|
|
||||
|
<header> |
||||
|
<h2 class="text-danger">@ViewData["Title"]</h2> |
||||
|
<p class="text-danger">This account has been locked out, please try again later.</p> |
||||
|
</header> |
||||
@ -0,0 +1,40 @@ |
|||||
|
@model LoginWith2faViewModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Two-factor authentication"; |
||||
|
} |
||||
|
|
||||
|
<h2>@ViewData["Title"]</h2> |
||||
|
<hr /> |
||||
|
<p>Your login is protected with an authenticator app. Enter your authenticator code below.</p> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-4"> |
||||
|
<form method="post" asp-route-returnUrl="@ViewData["ReturnUrl"]"> |
||||
|
<input asp-for="RememberMe" type="hidden" /> |
||||
|
<div asp-validation-summary="All" class="text-danger"></div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="TwoFactorCode"></label> |
||||
|
<input asp-for="TwoFactorCode" class="form-control" autocomplete="off" /> |
||||
|
<span asp-validation-for="TwoFactorCode" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<div class="checkbox"> |
||||
|
<label asp-for="RememberMachine"> |
||||
|
<input asp-for="RememberMachine" /> |
||||
|
@Html.DisplayNameFor(m => m.RememberMachine) |
||||
|
</label> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<button type="submit" class="btn btn-default">Log in</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
<p> |
||||
|
Don't have access to your authenticator device? You can |
||||
|
<a asp-action="LoginWithRecoveryCode" asp-route-returnUrl="@ViewData["ReturnUrl"]">log in with a recovery code</a>. |
||||
|
</p> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
@model ChangePasswordViewModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Change password"; |
||||
|
ViewData.AddActivePage(ManageNavPages.ChangePassword); |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
@Html.Partial("_StatusMessage", Model.StatusMessage) |
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"> |
||||
|
<form method="post"> |
||||
|
<div asp-validation-summary="All" class="text-danger"></div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="OldPassword"></label> |
||||
|
<input asp-for="OldPassword" class="form-control" /> |
||||
|
<span asp-validation-for="OldPassword" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="NewPassword"></label> |
||||
|
<input asp-for="NewPassword" class="form-control" /> |
||||
|
<span asp-validation-for="NewPassword" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="ConfirmPassword"></label> |
||||
|
<input asp-for="ConfirmPassword" class="form-control" /> |
||||
|
<span asp-validation-for="ConfirmPassword" class="text-danger"></span> |
||||
|
</div> |
||||
|
<button type="submit" class="btn btn-default">Update password</button> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
@{ |
||||
|
ViewData["Title"] = "Disable two-factor authentication (2FA)"; |
||||
|
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); |
||||
|
} |
||||
|
|
||||
|
<h2>@ViewData["Title"]</h2> |
||||
|
|
||||
|
<div class="alert alert-warning" role="alert"> |
||||
|
<p> |
||||
|
<span class="glyphicon glyphicon-warning-sign"></span> |
||||
|
<strong>This action only disables 2FA.</strong> |
||||
|
</p> |
||||
|
<p> |
||||
|
Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key |
||||
|
used in an authenticator app you should <a asp-action="ResetAuthenticatorWarning">reset your |
||||
|
authenticator keys.</a> |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<div> |
||||
|
<form asp-action="Disable2fa" method="post" class="form-group"> |
||||
|
<button class="btn btn-danger" type="submit">Disable 2FA</button> |
||||
|
</form> |
||||
|
</div> |
||||
@ -0,0 +1,52 @@ |
|||||
|
@model EnableAuthenticatorViewModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Enable authenticator"; |
||||
|
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
<div> |
||||
|
<p>To use an authenticator app go through the following steps:</p> |
||||
|
<ol class="list"> |
||||
|
<li> |
||||
|
<p> |
||||
|
Download a two-factor authenticator app like Microsoft Authenticator for |
||||
|
<a href="https://go.microsoft.com/fwlink/?Linkid=825071">Windows Phone</a>, |
||||
|
<a href="https://go.microsoft.com/fwlink/?Linkid=825072">Android</a> and |
||||
|
<a href="https://go.microsoft.com/fwlink/?Linkid=825073">iOS</a> or |
||||
|
Google Authenticator for |
||||
|
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en">Android</a> and |
||||
|
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">iOS</a>. |
||||
|
</p> |
||||
|
</li> |
||||
|
<li> |
||||
|
<p>Scan the QR Code or enter this key <kbd>@Model.SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p> |
||||
|
<div class="alert alert-info">To enable QR code generation please read our <a href="https://go.microsoft.com/fwlink/?Linkid=852423">documentation</a>.</div> |
||||
|
<div id="qrCode"></div> |
||||
|
<div id="qrCodeData" data-url="@Html.Raw(Model.AuthenticatorUri)"></div> |
||||
|
</li> |
||||
|
<li> |
||||
|
<p> |
||||
|
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. |
||||
|
</p> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"> |
||||
|
<form method="post"> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Code" class="control-label">Verification Code</label> |
||||
|
<input asp-for="Code" class="form-control" autocomplete="off" /> |
||||
|
<span asp-validation-for="Code" class="text-danger"></span> |
||||
|
</div> |
||||
|
<button type="submit" class="btn btn-default">Verify</button> |
||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</li> |
||||
|
</ol> |
||||
|
</div> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -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) |
||||
|
{ |
||||
|
<h4>Registered Logins</h4> |
||||
|
<table class="table"> |
||||
|
<tbody> |
||||
|
@foreach (var login in Model.CurrentLogins) |
||||
|
{ |
||||
|
<tr> |
||||
|
<td>@login.LoginProvider</td> |
||||
|
<td> |
||||
|
@if (Model.ShowRemoveButton) |
||||
|
{ |
||||
|
<form asp-action="RemoveLogin" method="post"> |
||||
|
<div> |
||||
|
<input asp-for="@login.LoginProvider" name="LoginProvider" type="hidden" /> |
||||
|
<input asp-for="@login.ProviderKey" name="ProviderKey" type="hidden" /> |
||||
|
<button type="submit" class="btn btn-default" title="Remove this @login.LoginProvider login from your account">Remove</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
@: |
||||
|
} |
||||
|
</td> |
||||
|
</tr> |
||||
|
} |
||||
|
</tbody> |
||||
|
</table> |
||||
|
} |
||||
|
@if (Model.OtherLogins?.Count > 0) |
||||
|
{ |
||||
|
<h4>Add another service to log in.</h4> |
||||
|
<hr /> |
||||
|
<form asp-action="LinkLogin" method="post" class="form-horizontal"> |
||||
|
<div id="socialLoginList"> |
||||
|
<p> |
||||
|
@foreach (var provider in Model.OtherLogins) |
||||
|
{ |
||||
|
<button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> |
||||
|
} |
||||
|
</p> |
||||
|
</div> |
||||
|
</form> |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
@model GenerateRecoveryCodesViewModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Recovery codes"; |
||||
|
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
<div class="alert alert-warning" role="alert"> |
||||
|
<p> |
||||
|
<span class="glyphicon glyphicon-warning-sign"></span> |
||||
|
<strong>Put these codes in a safe place.</strong> |
||||
|
</p> |
||||
|
<p> |
||||
|
If you lose your device and don't have the recovery codes you will lose access to your account. |
||||
|
</p> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-12"> |
||||
|
@for (var row = 0; row < Model.RecoveryCodes.Count(); row += 2) |
||||
|
{ |
||||
|
<code>@Model.RecoveryCodes[row]</code><text> </text><code>@Model.RecoveryCodes[row + 1]</code><br /> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,45 @@ |
|||||
|
@model IndexViewModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Profile"; |
||||
|
ViewData.AddActivePage(ManageNavPages.Index); |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
@Html.Partial("_StatusMessage", Model.StatusMessage) |
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"> |
||||
|
<form method="post"> |
||||
|
<div asp-validation-summary="All" class="text-danger"></div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Username"></label> |
||||
|
<input asp-for="Username" class="form-control" disabled /> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Email"></label> |
||||
|
@if (Model.IsEmailConfirmed) |
||||
|
{ |
||||
|
<div class="input-group"> |
||||
|
<input asp-for="Email" class="form-control" /> |
||||
|
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span> |
||||
|
</div> |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
<input asp-for="Email" class="form-control" /> |
||||
|
<button asp-action="SendVerificationEmail" class="btn btn-link">Send verification email</button> |
||||
|
} |
||||
|
<span asp-validation-for="Email" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="PhoneNumber"></label> |
||||
|
<input asp-for="PhoneNumber" class="form-control" /> |
||||
|
<span asp-validation-for="PhoneNumber" class="text-danger"></span> |
||||
|
</div> |
||||
|
<button type="submit" class="btn btn-default">Save</button> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
using System; |
||||
|
using Microsoft.AspNetCore.Mvc.Rendering; |
||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
@{ |
||||
|
ViewData["Title"] = "Reset authenticator key"; |
||||
|
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
<div class="alert alert-warning" role="alert"> |
||||
|
<p> |
||||
|
<span class="glyphicon glyphicon-warning-sign"></span> |
||||
|
<strong>If you reset your authenticator key your authenticator app will not work until you reconfigure it.</strong> |
||||
|
</p> |
||||
|
<p> |
||||
|
This process disables 2FA until you verify your authenticator app and will also reset your 2FA recovery codes. |
||||
|
If you do not complete your authenticator app configuration you may lose access to your account. |
||||
|
</p> |
||||
|
</div> |
||||
|
<div> |
||||
|
<form asp-action="ResetAuthenticator" method="post" class="form-group"> |
||||
|
<button class="btn btn-danger" type="submit">Reset authenticator key</button> |
||||
|
</form> |
||||
|
</div> |
||||
@ -0,0 +1,34 @@ |
|||||
|
@model SetPasswordViewModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Set password"; |
||||
|
ViewData.AddActivePage(ManageNavPages.ChangePassword); |
||||
|
} |
||||
|
|
||||
|
<h4>Set your password</h4> |
||||
|
@Html.Partial("_StatusMessage", Model.StatusMessage) |
||||
|
<p class="text-info"> |
||||
|
You do not have a local username/password for this site. Add a local |
||||
|
account so you can log in without an external login. |
||||
|
</p> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"> |
||||
|
<form method="post"> |
||||
|
<div asp-validation-summary="All" class="text-danger"></div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="NewPassword"></label> |
||||
|
<input asp-for="NewPassword" class="form-control" /> |
||||
|
<span asp-validation-for="NewPassword" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="ConfirmPassword"></label> |
||||
|
<input asp-for="ConfirmPassword" class="form-control" /> |
||||
|
<span asp-validation-for="ConfirmPassword" class="text-danger"></span> |
||||
|
</div> |
||||
|
<button type="submit" class="btn btn-default">Set password</button> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
@model TwoFactorAuthenticationViewModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Two-factor authentication"; |
||||
|
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
@if (Model.Is2faEnabled) |
||||
|
{ |
||||
|
if (Model.RecoveryCodesLeft == 0) |
||||
|
{ |
||||
|
<div class="alert alert-danger"> |
||||
|
<strong>You have no recovery codes left.</strong> |
||||
|
<p>You must <a asp-action="GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p> |
||||
|
</div> |
||||
|
} |
||||
|
else if (Model.RecoveryCodesLeft == 1) |
||||
|
{ |
||||
|
<div class="alert alert-danger"> |
||||
|
<strong>You have 1 recovery code left.</strong> |
||||
|
<p>You can <a asp-action="GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p> |
||||
|
</div> |
||||
|
} |
||||
|
else if (Model.RecoveryCodesLeft <= 3) |
||||
|
{ |
||||
|
<div class="alert alert-warning"> |
||||
|
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong> |
||||
|
<p>You should <a asp-action="GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
<a asp-action="Disable2faWarning" class="btn btn-default">Disable 2FA</a> |
||||
|
<a asp-action="GenerateRecoveryCodes" class="btn btn-default">Reset recovery codes</a> |
||||
|
} |
||||
|
|
||||
|
<h5>Authenticator app</h5> |
||||
|
@if (!Model.HasAuthenticator) |
||||
|
{ |
||||
|
<a asp-action="EnableAuthenticator" class="btn btn-default">Add authenticator app</a> |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
<a asp-action="EnableAuthenticator" class="btn btn-default">Configure authenticator app</a> |
||||
|
<a asp-action="ResetAuthenticatorWarning" class="btn btn-default">Reset authenticator key</a> |
||||
|
} |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
@{ |
||||
|
Layout = "/Views/Shared/_Layout.cshtml"; |
||||
|
} |
||||
|
|
||||
|
<h2>Manage your account</h2> |
||||
|
|
||||
|
<div> |
||||
|
<h4>Change your account settings</h4> |
||||
|
<hr /> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-3"> |
||||
|
@await Html.PartialAsync("_ManageNav") |
||||
|
</div> |
||||
|
<div class="col-md-9"> |
||||
|
@RenderBody() |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@RenderSection("Scripts", required: false) |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,15 @@ |
|||||
|
@inject SignInManager<ApplicationUser> SignInManager |
||||
|
@{ |
||||
|
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); |
||||
|
} |
||||
|
|
||||
|
<ul class="nav nav-pills nav-stacked"> |
||||
|
<li class="@ManageNavPages.IndexNavClass(ViewContext)"><a asp-action="Index">Profile</a></li> |
||||
|
<li class="@ManageNavPages.ChangePasswordNavClass(ViewContext)"><a asp-action="ChangePassword">Password</a></li> |
||||
|
@if (hasExternalLogins) |
||||
|
{ |
||||
|
<li class="@ManageNavPages.ExternalLoginsNavClass(ViewContext)"><a asp-action="ExternalLogins">External logins</a></li> |
||||
|
} |
||||
|
<li class="@ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)"><a asp-action="TwoFactorAuthentication">Two-factor authentication</a></li> |
||||
|
</ul> |
||||
|
|
||||
@ -0,0 +1,10 @@ |
|||||
|
@model string |
||||
|
|
||||
|
@if (!String.IsNullOrEmpty(Model)) |
||||
|
{ |
||||
|
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; |
||||
|
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert"> |
||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button> |
||||
|
@Model |
||||
|
</div> |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
@using Microsoft.eShopWeb.Views.Manage |
||||
@ -1,3 +1,7 @@ |
|||||
@using Microsoft.eShopWeb |
@using Microsoft.eShopWeb |
||||
@using Microsoft.eShopWeb.ViewModels |
@using Microsoft.eShopWeb.ViewModels |
||||
|
@using Microsoft.eShopWeb.ViewModels.Account |
||||
|
@using Microsoft.eShopWeb.ViewModels.Manage |
||||
|
@using Microsoft.AspNetCore.Identity |
||||
|
@using Infrastructure.Identity |
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
||||
|
|||||
@ -0,0 +1,31 @@ |
|||||
|
using ApplicationCore.Interfaces; |
||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Controllers |
||||
|
{ |
||||
|
[Route("[controller]/[action]")]
|
||||
|
public class AccountController : Controller |
||||
|
{ |
||||
|
private readonly SignInManager<ApplicationUser> _signInManager; |
||||
|
private readonly IAppLogger<AccountController> _logger; |
||||
|
|
||||
|
public AccountController(SignInManager<ApplicationUser> signInManager, |
||||
|
IAppLogger<AccountController> logger) |
||||
|
{ |
||||
|
_signInManager = signInManager; |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[ValidateAntiForgeryToken] |
||||
|
public async Task<IActionResult> Logout() |
||||
|
{ |
||||
|
await _signInManager.SignOutAsync(); |
||||
|
_logger.LogInformation("User logged out."); |
||||
|
return RedirectToPage("/Index"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using ApplicationCore.Interfaces; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Mvc |
||||
|
{ |
||||
|
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 <a href='{HtmlEncoder.Default.Encode(link)}'>clicking here</a>."); |
||||
|
} |
||||
|
|
||||
|
public static Task SendResetPasswordAsync(this IEmailSender emailSender, string email, string callbackUrl) |
||||
|
{ |
||||
|
return emailSender.SendEmailAsync(email, "Reset Password", |
||||
|
$"Please reset your password by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
namespace Microsoft.AspNetCore.Mvc |
||||
|
{ |
||||
|
public static class UrlHelperExtensions |
||||
|
{ |
||||
|
public static string GetLocalUrl(this IUrlHelper urlHelper, string localUrl) |
||||
|
{ |
||||
|
if (!urlHelper.IsLocalUrl(localUrl)) |
||||
|
{ |
||||
|
return urlHelper.Page("/Index"); |
||||
|
} |
||||
|
|
||||
|
return localUrl; |
||||
|
} |
||||
|
|
||||
|
public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme) |
||||
|
{ |
||||
|
return urlHelper.Page( |
||||
|
"/Account/ConfirmEmail", |
||||
|
pageHandler: null, |
||||
|
values: new { userId, code }, |
||||
|
protocol: scheme); |
||||
|
} |
||||
|
|
||||
|
public static string ResetPasswordCallbackLink(this IUrlHelper urlHelper, string userId, string code, string scheme) |
||||
|
{ |
||||
|
return urlHelper.Page( |
||||
|
"/Account/ResetPassword", |
||||
|
pageHandler: null, |
||||
|
values: new { userId, code }, |
||||
|
protocol: scheme); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
@page |
||||
|
@model LoginWith2faModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Two-factor authentication"; |
||||
|
} |
||||
|
|
||||
|
<h2>@ViewData["Title"]</h2> |
||||
|
<hr /> |
||||
|
<p>Your login is protected with an authenticator app. Enter your authenticator code below.</p> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-4"> |
||||
|
<form method="post" asp-route-returnUrl="@Model.ReturnUrl"> |
||||
|
<input asp-for="RememberMe" type="hidden" /> |
||||
|
<div asp-validation-summary="All" class="text-danger"></div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Input.TwoFactorCode"></label> |
||||
|
<input asp-for="Input.TwoFactorCode" class="form-control" autocomplete="off" /> |
||||
|
<span asp-validation-for="Input.TwoFactorCode" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<div class="checkbox"> |
||||
|
<label asp-for="Input.RememberMachine"> |
||||
|
<input asp-for="Input.RememberMachine" /> |
||||
|
@Html.DisplayNameFor(m => m.Input.RememberMachine) |
||||
|
</label> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<button type="submit" class="btn btn-default">Log in</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
<p> |
||||
|
Don't have access to your authenticator device? You can |
||||
|
<a asp-page="./LoginWithRecoveryCode" asp-route-returnUrl="@Model.ReturnUrl">log in with a recovery code</a>. |
||||
|
</p> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,94 @@ |
|||||
|
using ApplicationCore.Interfaces; |
||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using System; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Pages.Account |
||||
|
{ |
||||
|
public class LoginWith2faModel : PageModel |
||||
|
{ |
||||
|
private readonly SignInManager<ApplicationUser> _signInManager; |
||||
|
private readonly IAppLogger<LoginWith2faModel> _logger; |
||||
|
|
||||
|
public LoginWith2faModel(SignInManager<ApplicationUser> signInManager, |
||||
|
IAppLogger<LoginWith2faModel> logger) |
||||
|
{ |
||||
|
_signInManager = signInManager; |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
[BindProperty] |
||||
|
public InputModel Input { get; set; } |
||||
|
|
||||
|
public bool RememberMe { get; set; } |
||||
|
|
||||
|
public string ReturnUrl { get; set; } |
||||
|
|
||||
|
public class InputModel |
||||
|
{ |
||||
|
[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 async Task<IActionResult> OnGetAsync(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."); |
||||
|
} |
||||
|
|
||||
|
ReturnUrl = returnUrl; |
||||
|
RememberMe = rememberMe; |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnPostAsync(bool rememberMe, string returnUrl = null) |
||||
|
{ |
||||
|
if (!ModelState.IsValid) |
||||
|
{ |
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load two-factor authentication user."); |
||||
|
} |
||||
|
|
||||
|
var authenticatorCode = Input.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty); |
||||
|
|
||||
|
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, Input.RememberMachine); |
||||
|
|
||||
|
if (result.Succeeded) |
||||
|
{ |
||||
|
_logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id); |
||||
|
return LocalRedirect(Url.GetLocalUrl(returnUrl)); |
||||
|
} |
||||
|
else if (result.IsLockedOut) |
||||
|
{ |
||||
|
_logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); |
||||
|
return RedirectToPage("./Lockout"); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id); |
||||
|
ModelState.AddModelError(string.Empty, "Invalid authenticator code."); |
||||
|
return Page(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
@page |
||||
|
@model LoginWithRecoveryCodeModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Recovery code verification"; |
||||
|
} |
||||
|
|
||||
|
<h2>@ViewData["Title"]</h2> |
||||
|
<hr /> |
||||
|
<p> |
||||
|
You have requested to log in with a recovery code. This login will not be remembered until you provide |
||||
|
an authenticator app code at log in or disable 2FA and log in again. |
||||
|
</p> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-4"> |
||||
|
<form method="post"> |
||||
|
<div asp-validation-summary="All" class="text-danger"></div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Input.RecoveryCode"></label> |
||||
|
<input asp-for="Input.RecoveryCode" class="form-control" autocomplete="off" /> |
||||
|
<span asp-validation-for="Input.RecoveryCode" class="text-danger"></span> |
||||
|
</div> |
||||
|
<button type="submit" class="btn btn-default">Log in</button> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
using ApplicationCore.Interfaces; |
||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using System; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Pages.Account.Manage |
||||
|
{ |
||||
|
public class LoginWithRecoveryCodeModel : PageModel |
||||
|
{ |
||||
|
private readonly SignInManager<ApplicationUser> _signInManager; |
||||
|
private readonly IAppLogger<LoginWithRecoveryCodeModel> _logger; |
||||
|
|
||||
|
public LoginWithRecoveryCodeModel(SignInManager<ApplicationUser> signInManager, |
||||
|
IAppLogger<LoginWithRecoveryCodeModel> logger) |
||||
|
{ |
||||
|
_signInManager = signInManager; |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
[BindProperty] |
||||
|
public InputModel Input { get; set; } |
||||
|
|
||||
|
public string ReturnUrl { get; set; } |
||||
|
|
||||
|
public class InputModel |
||||
|
{ |
||||
|
[BindProperty] |
||||
|
[Required] |
||||
|
[DataType(DataType.Text)] |
||||
|
[Display(Name = "Recovery Code")] |
||||
|
public string RecoveryCode { get; set; } |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnGetAsync(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."); |
||||
|
} |
||||
|
|
||||
|
ReturnUrl = returnUrl; |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnPostAsync(string returnUrl = null) |
||||
|
{ |
||||
|
if (!ModelState.IsValid) |
||||
|
{ |
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load two-factor authentication user."); |
||||
|
} |
||||
|
|
||||
|
var recoveryCode = Input.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 LocalRedirect(Url.GetLocalUrl(returnUrl)); |
||||
|
} |
||||
|
if (result.IsLockedOut) |
||||
|
{ |
||||
|
_logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); |
||||
|
return RedirectToPage("./Lockout"); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id); |
||||
|
ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); |
||||
|
return Page(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
@page |
||||
|
@model ChangePasswordModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Change password"; |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
@Html.Partial("_StatusMessage", Model.StatusMessage) |
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"> |
||||
|
<form method="post"> |
||||
|
<div asp-validation-summary="All" class="text-danger"></div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Input.OldPassword"></label> |
||||
|
<input asp-for="Input.OldPassword" class="form-control" /> |
||||
|
<span asp-validation-for="Input.OldPassword" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Input.NewPassword"></label> |
||||
|
<input asp-for="Input.NewPassword" class="form-control" /> |
||||
|
<span asp-validation-for="Input.NewPassword" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Input.ConfirmPassword"></label> |
||||
|
<input asp-for="Input.ConfirmPassword" class="form-control" /> |
||||
|
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> |
||||
|
</div> |
||||
|
<button type="submit" class="btn btn-default">Update password</button> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using System; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Pages.Account.Manage |
||||
|
{ |
||||
|
public class ChangePasswordModel : PageModel |
||||
|
{ |
||||
|
private readonly UserManager<ApplicationUser> _userManager; |
||||
|
private readonly SignInManager<ApplicationUser> _signInManager; |
||||
|
private readonly ILogger<ChangePasswordModel> _logger; |
||||
|
|
||||
|
public ChangePasswordModel( |
||||
|
UserManager<ApplicationUser> userManager, |
||||
|
SignInManager<ApplicationUser> signInManager, |
||||
|
ILogger<ChangePasswordModel> logger) |
||||
|
{ |
||||
|
_userManager = userManager; |
||||
|
_signInManager = signInManager; |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
[BindProperty] |
||||
|
public InputModel Input { get; set; } |
||||
|
|
||||
|
[TempData] |
||||
|
public string StatusMessage { get; set; } |
||||
|
|
||||
|
public class InputModel |
||||
|
{ |
||||
|
[Required] |
||||
|
[DataType(DataType.Password)] |
||||
|
[Display(Name = "Current password")] |
||||
|
public string OldPassword { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] |
||||
|
[DataType(DataType.Password)] |
||||
|
[Display(Name = "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 async Task<IActionResult> OnGetAsync() |
||||
|
{ |
||||
|
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 RedirectToPage("./SetPassword"); |
||||
|
} |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnPostAsync() |
||||
|
{ |
||||
|
if (!ModelState.IsValid) |
||||
|
{ |
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); |
||||
|
if (!changePasswordResult.Succeeded) |
||||
|
{ |
||||
|
foreach (var error in changePasswordResult.Errors) |
||||
|
{ |
||||
|
ModelState.AddModelError(string.Empty, error.Description); |
||||
|
} |
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
await _signInManager.SignInAsync(user, isPersistent: false); |
||||
|
_logger.LogInformation("User changed their password successfully."); |
||||
|
StatusMessage = "Your password has been changed."; |
||||
|
|
||||
|
return RedirectToPage(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
@page |
||||
|
@model Disable2faModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Disable two-factor authentication (2FA)"; |
||||
|
ViewData["ActivePage"] = "TwoFactorAuthentication"; |
||||
|
} |
||||
|
|
||||
|
<h2>@ViewData["Title"]</h2> |
||||
|
|
||||
|
<div class="alert alert-warning" role="alert"> |
||||
|
<p> |
||||
|
<span class="glyphicon glyphicon-warning-sign"></span> |
||||
|
<strong>This action only disables 2FA.</strong> |
||||
|
</p> |
||||
|
<p> |
||||
|
Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key |
||||
|
used in an authenticator app you should <a asp-page="./ResetAuthenticator">reset your authenticator keys.</a> |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<div> |
||||
|
<form method="post" class="form-group"> |
||||
|
<button class="btn btn-danger" type="submit">Disable 2FA</button> |
||||
|
</form> |
||||
|
</div> |
||||
@ -0,0 +1,59 @@ |
|||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Pages.Account.Manage |
||||
|
{ |
||||
|
public class Disable2faModel : PageModel |
||||
|
{ |
||||
|
private readonly UserManager<ApplicationUser> _userManager; |
||||
|
private readonly ILogger<Disable2faModel> _logger; |
||||
|
|
||||
|
public Disable2faModel( |
||||
|
UserManager<ApplicationUser> userManager, |
||||
|
ILogger<Disable2faModel> logger) |
||||
|
{ |
||||
|
_userManager = userManager; |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnGet() |
||||
|
{ |
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
if (!await _userManager.GetTwoFactorEnabledAsync(user)) |
||||
|
{ |
||||
|
throw new ApplicationException($"Cannot disable 2FA for user with ID '{_userManager.GetUserId(User)}' as it's not currently enabled."); |
||||
|
} |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnPostAsync() |
||||
|
{ |
||||
|
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 occurred disabling 2FA for user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
_logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User)); |
||||
|
|
||||
|
return RedirectToPage("./TwoFactorAuthentication"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
@page |
||||
|
@model EnableAuthenticatorModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Configure authenticator app"; |
||||
|
ViewData["ActivePage"] = "TwoFactorAuthentication"; |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
<div> |
||||
|
<p>To use an authenticator app go through the following steps:</p> |
||||
|
<ol class="list"> |
||||
|
<li> |
||||
|
<p> |
||||
|
Download a two-factor authenticator app like Microsoft Authenticator for |
||||
|
<a href="https://go.microsoft.com/fwlink/?Linkid=825071">Windows Phone</a>, |
||||
|
<a href="https://go.microsoft.com/fwlink/?Linkid=825072">Android</a> and |
||||
|
<a href="https://go.microsoft.com/fwlink/?Linkid=825073">iOS</a> or |
||||
|
Google Authenticator for |
||||
|
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en">Android</a> and |
||||
|
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">iOS</a>. |
||||
|
</p> |
||||
|
</li> |
||||
|
<li> |
||||
|
<p>Scan the QR Code or enter this key <kbd>@Model.SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p> |
||||
|
<div class="alert alert-info">To enable QR code generation please read our <a href="https://go.microsoft.com/fwlink/?Linkid=852423">documentation</a>.</div> |
||||
|
<div id="qrCode"></div> |
||||
|
<div id="qrCodeData" data-url="@Html.Raw(Model.AuthenticatorUri)"></div> |
||||
|
</li> |
||||
|
<li> |
||||
|
<p> |
||||
|
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. |
||||
|
</p> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"> |
||||
|
<form method="post"> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Input.Code" class="control-label">Verification Code</label> |
||||
|
<input asp-for="Input.Code" class="form-control" autocomplete="off" /> |
||||
|
<span asp-validation-for="Input.Code" class="text-danger"></span> |
||||
|
</div> |
||||
|
<button type="submit" class="btn btn-default">Verify</button> |
||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</li> |
||||
|
</ol> |
||||
|
</div> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,135 @@ |
|||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using System; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using System.Text; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Pages.Account.Manage |
||||
|
{ |
||||
|
public class EnableAuthenticatorModel : PageModel |
||||
|
{ |
||||
|
private readonly UserManager<ApplicationUser> _userManager; |
||||
|
private readonly ILogger<EnableAuthenticatorModel> _logger; |
||||
|
private readonly UrlEncoder _urlEncoder; |
||||
|
|
||||
|
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; |
||||
|
|
||||
|
public EnableAuthenticatorModel( |
||||
|
UserManager<ApplicationUser> userManager, |
||||
|
ILogger<EnableAuthenticatorModel> logger, |
||||
|
UrlEncoder urlEncoder) |
||||
|
{ |
||||
|
_userManager = userManager; |
||||
|
_logger = logger; |
||||
|
_urlEncoder = urlEncoder; |
||||
|
} |
||||
|
|
||||
|
public string SharedKey { get; set; } |
||||
|
|
||||
|
public string AuthenticatorUri { get; set; } |
||||
|
|
||||
|
[BindProperty] |
||||
|
public InputModel Input { get; set; } |
||||
|
|
||||
|
public class InputModel |
||||
|
{ |
||||
|
[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; } |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnGetAsync() |
||||
|
{ |
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
await LoadSharedKeyAndQrCodeUriAsync(user); |
||||
|
if (string.IsNullOrEmpty(SharedKey)) |
||||
|
{ |
||||
|
await _userManager.ResetAuthenticatorKeyAsync(user); |
||||
|
await LoadSharedKeyAndQrCodeUriAsync(user); |
||||
|
} |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnPostAsync() |
||||
|
{ |
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
if (!ModelState.IsValid) |
||||
|
{ |
||||
|
await LoadSharedKeyAndQrCodeUriAsync(user); |
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
// Strip spaces and hypens
|
||||
|
var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty); |
||||
|
|
||||
|
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( |
||||
|
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); |
||||
|
|
||||
|
if (!is2faTokenValid) |
||||
|
{ |
||||
|
ModelState.AddModelError("Input.Code", "Verification code is invalid."); |
||||
|
await LoadSharedKeyAndQrCodeUriAsync(user); |
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
await _userManager.SetTwoFactorEnabledAsync(user, true); |
||||
|
_logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", user.Id); |
||||
|
return RedirectToPage("./GenerateRecoveryCodes"); |
||||
|
} |
||||
|
|
||||
|
private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user) |
||||
|
{ |
||||
|
// Load the authenticator key & QR code URI to display on the form
|
||||
|
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); |
||||
|
if (!string.IsNullOrEmpty(unformattedKey)) |
||||
|
{ |
||||
|
SharedKey = FormatKey(unformattedKey); |
||||
|
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private string FormatKey(string unformattedKey) |
||||
|
{ |
||||
|
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 string GenerateQrCodeUri(string email, string unformattedKey) |
||||
|
{ |
||||
|
return string.Format( |
||||
|
AuthenicatorUriFormat, |
||||
|
_urlEncoder.Encode("RazorPagesAuthSample2"), |
||||
|
_urlEncoder.Encode(email), |
||||
|
unformattedKey); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
@page |
||||
|
@model GenerateRecoveryCodesModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Recovery codes"; |
||||
|
ViewData["ActivePage"] = "TwoFactorAuthentication"; |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
<div class="alert alert-warning" role="alert"> |
||||
|
<p> |
||||
|
<span class="glyphicon glyphicon-warning-sign"></span> |
||||
|
<strong>Put these codes in a safe place.</strong> |
||||
|
</p> |
||||
|
<p> |
||||
|
If you lose your device and don't have the recovery codes you will lose access to your account. |
||||
|
</p> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-12"> |
||||
|
@for (var row = 0; row < Model.RecoveryCodes.Count(); row += 2) |
||||
|
{ |
||||
|
<code>@Model.RecoveryCodes[row]</code><text> </text><code>@Model.RecoveryCodes[row + 1]</code><br /> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,48 @@ |
|||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Pages.Account.Manage |
||||
|
{ |
||||
|
public class GenerateRecoveryCodesModel : PageModel |
||||
|
{ |
||||
|
private readonly UserManager<ApplicationUser> _userManager; |
||||
|
private readonly ILogger<GenerateRecoveryCodesModel> _logger; |
||||
|
|
||||
|
public GenerateRecoveryCodesModel( |
||||
|
UserManager<ApplicationUser> userManager, |
||||
|
ILogger<GenerateRecoveryCodesModel> logger) |
||||
|
{ |
||||
|
_userManager = userManager; |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
public string[] RecoveryCodes { get; set; } |
||||
|
|
||||
|
public async Task<IActionResult> OnGetAsync() |
||||
|
{ |
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
if (!user.TwoFactorEnabled) |
||||
|
{ |
||||
|
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); |
||||
|
RecoveryCodes = recoveryCodes.ToArray(); |
||||
|
|
||||
|
_logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", user.Id); |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
@page |
||||
|
@model IndexModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Profile"; |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
@Html.Partial("_StatusMessage", Model.StatusMessage) |
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"> |
||||
|
<form method="post"> |
||||
|
<div asp-validation-summary="All" class="text-danger"></div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Username"></label> |
||||
|
<input asp-for="Username" class="form-control" disabled /> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Input.Email"></label> |
||||
|
@if (Model.IsEmailConfirmed) |
||||
|
{ |
||||
|
<div class="input-group"> |
||||
|
<input asp-for="Input.Email" class="form-control" /> |
||||
|
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span> |
||||
|
</div> |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
<input asp-for="Input.Email" class="form-control" /> |
||||
|
<button asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button> |
||||
|
} |
||||
|
<span asp-validation-for="Input.Email" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Input.PhoneNumber"></label> |
||||
|
<input asp-for="Input.PhoneNumber" class="form-control" /> |
||||
|
<span asp-validation-for="Input.PhoneNumber" class="text-danger"></span> |
||||
|
</div> |
||||
|
<button type="submit" class="btn btn-default">Save</button> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,125 @@ |
|||||
|
using ApplicationCore.Interfaces; |
||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using System; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Pages.Account.Manage |
||||
|
{ |
||||
|
public partial class IndexModel : PageModel |
||||
|
{ |
||||
|
private readonly UserManager<ApplicationUser> _userManager; |
||||
|
private readonly SignInManager<ApplicationUser> _signInManager; |
||||
|
private readonly IEmailSender _emailSender; |
||||
|
|
||||
|
public IndexModel( |
||||
|
UserManager<ApplicationUser> userManager, |
||||
|
SignInManager<ApplicationUser> signInManager, |
||||
|
IEmailSender emailSender) |
||||
|
{ |
||||
|
_userManager = userManager; |
||||
|
_signInManager = signInManager; |
||||
|
_emailSender = emailSender; |
||||
|
} |
||||
|
|
||||
|
public string Username { get; set; } |
||||
|
|
||||
|
public bool IsEmailConfirmed { get; set; } |
||||
|
|
||||
|
[TempData] |
||||
|
public string StatusMessage { get; set; } |
||||
|
|
||||
|
[BindProperty] |
||||
|
public InputModel Input { get; set; } |
||||
|
|
||||
|
public class InputModel |
||||
|
{ |
||||
|
[Required] |
||||
|
[EmailAddress] |
||||
|
public string Email { get; set; } |
||||
|
|
||||
|
[Phone] |
||||
|
[Display(Name = "Phone number")] |
||||
|
public string PhoneNumber { get; set; } |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnGetAsync() |
||||
|
{ |
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
Username = user.UserName; |
||||
|
|
||||
|
Input = new InputModel |
||||
|
{ |
||||
|
Email = user.Email, |
||||
|
PhoneNumber = user.PhoneNumber |
||||
|
}; |
||||
|
|
||||
|
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user); |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnPostAsync() |
||||
|
{ |
||||
|
if (!ModelState.IsValid) |
||||
|
{ |
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
if (Input.Email != user.Email) |
||||
|
{ |
||||
|
var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email); |
||||
|
if (!setEmailResult.Succeeded) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (Input.PhoneNumber != user.PhoneNumber) |
||||
|
{ |
||||
|
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.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 RedirectToPage(); |
||||
|
} |
||||
|
public async Task<IActionResult> OnPostSendVerificationEmailAsync() |
||||
|
{ |
||||
|
if (!ModelState.IsValid) |
||||
|
{ |
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); |
||||
|
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); |
||||
|
await _emailSender.SendEmailConfirmationAsync(user.Email, callbackUrl); |
||||
|
|
||||
|
StatusMessage = "Verification email sent. Please check your email."; |
||||
|
return RedirectToPage(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
using Microsoft.AspNetCore.Mvc.Rendering; |
||||
|
using System; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Pages.Account.Manage |
||||
|
{ |
||||
|
public static class ManageNavPages |
||||
|
{ |
||||
|
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 |
||||
|
?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); |
||||
|
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
@page |
||||
|
@model ResetAuthenticatorModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Reset authenticator key"; |
||||
|
ViewData["ActivePage"] = "TwoFactorAuthentication"; |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
<div class="alert alert-warning" role="alert"> |
||||
|
<p> |
||||
|
<span class="glyphicon glyphicon-warning-sign"></span> |
||||
|
<strong>If you reset your authenticator key your authenticator app will not work until you reconfigure it.</strong> |
||||
|
</p> |
||||
|
<p> |
||||
|
This process disables 2FA until you verify your authenticator app and will also reset your 2FA recovery codes. |
||||
|
If you do not complete your authenticator app configuration you may lose access to your account. |
||||
|
</p> |
||||
|
</div> |
||||
|
<div> |
||||
|
<form method="post" class="form-group"> |
||||
|
<button class="btn btn-danger" type="submit">Reset authenticator key</button> |
||||
|
</form> |
||||
|
</div> |
||||
@ -0,0 +1,49 @@ |
|||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Pages.Account.Manage |
||||
|
{ |
||||
|
public class ResetAuthenticatorModel : PageModel |
||||
|
{ |
||||
|
UserManager<ApplicationUser> _userManager; |
||||
|
ILogger<ResetAuthenticatorModel> _logger; |
||||
|
|
||||
|
public ResetAuthenticatorModel( |
||||
|
UserManager<ApplicationUser> userManager, |
||||
|
ILogger<ResetAuthenticatorModel> logger) |
||||
|
{ |
||||
|
_userManager = userManager; |
||||
|
_logger = logger; |
||||
|
} |
||||
|
public async Task<IActionResult> OnGet() |
||||
|
{ |
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnPostAsync() |
||||
|
{ |
||||
|
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 RedirectToPage("./EnableAuthenticator"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
@page |
||||
|
@model SetPasswordModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Set password"; |
||||
|
ViewData["ActivePage"] = "ChangePassword"; |
||||
|
} |
||||
|
|
||||
|
<h4>Set your password</h4> |
||||
|
@Html.Partial("_StatusMessage", Model.StatusMessage) |
||||
|
<p class="text-info"> |
||||
|
You do not have a local username/password for this site. Add a local |
||||
|
account so you can log in without an external login. |
||||
|
</p> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"> |
||||
|
<form method="post"> |
||||
|
<div asp-validation-summary="All" class="text-danger"></div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Input.NewPassword"></label> |
||||
|
<input asp-for="Input.NewPassword" class="form-control" /> |
||||
|
<span asp-validation-for="Input.NewPassword" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="Input.ConfirmPassword"></label> |
||||
|
<input asp-for="Input.ConfirmPassword" class="form-control" /> |
||||
|
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> |
||||
|
</div> |
||||
|
<button type="submit" class="btn btn-default">Set password</button> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using System; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Pages.Account.Manage |
||||
|
{ |
||||
|
public class SetPasswordModel : PageModel |
||||
|
{ |
||||
|
private readonly UserManager<ApplicationUser> _userManager; |
||||
|
private readonly SignInManager<ApplicationUser> _signInManager; |
||||
|
|
||||
|
public SetPasswordModel( |
||||
|
UserManager<ApplicationUser> userManager, |
||||
|
SignInManager<ApplicationUser> signInManager) |
||||
|
{ |
||||
|
_userManager = userManager; |
||||
|
_signInManager = signInManager; |
||||
|
} |
||||
|
|
||||
|
[BindProperty] |
||||
|
public InputModel Input { get; set; } |
||||
|
|
||||
|
[TempData] |
||||
|
public string StatusMessage { get; set; } |
||||
|
|
||||
|
public class InputModel |
||||
|
{ |
||||
|
[Required] |
||||
|
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} 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 async Task<IActionResult> OnGetAsync() |
||||
|
{ |
||||
|
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 RedirectToPage("./ChangePassword"); |
||||
|
} |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IActionResult> OnPostAsync() |
||||
|
{ |
||||
|
if (!ModelState.IsValid) |
||||
|
{ |
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
var addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword); |
||||
|
if (!addPasswordResult.Succeeded) |
||||
|
{ |
||||
|
foreach (var error in addPasswordResult.Errors) |
||||
|
{ |
||||
|
ModelState.AddModelError(string.Empty, error.Description); |
||||
|
} |
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
await _signInManager.SignInAsync(user, isPersistent: false); |
||||
|
StatusMessage = "Your password has been set."; |
||||
|
|
||||
|
return RedirectToPage(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
@page |
||||
|
@model TwoFactorAuthenticationModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Two-factor authentication (2FA)"; |
||||
|
} |
||||
|
|
||||
|
<h4>@ViewData["Title"]</h4> |
||||
|
@if (Model.Is2faEnabled) |
||||
|
{ |
||||
|
if (Model.RecoveryCodesLeft == 0) |
||||
|
{ |
||||
|
<div class="alert alert-danger"> |
||||
|
<strong>You have no recovery codes left.</strong> |
||||
|
<p>You must <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p> |
||||
|
</div> |
||||
|
} |
||||
|
else if (Model.RecoveryCodesLeft == 1) |
||||
|
{ |
||||
|
<div class="alert alert-danger"> |
||||
|
<strong>You have 1 recovery code left.</strong> |
||||
|
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p> |
||||
|
</div> |
||||
|
} |
||||
|
else if (Model.RecoveryCodesLeft <= 3) |
||||
|
{ |
||||
|
<div class="alert alert-warning"> |
||||
|
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong> |
||||
|
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
<a asp-page="./Disable2fa" class="btn btn-default">Disable 2FA</a> |
||||
|
<a asp-page="./GenerateRecoveryCodes" class="btn btn-default">Reset recovery codes</a> |
||||
|
} |
||||
|
|
||||
|
<h5>Authenticator app</h5> |
||||
|
@if (!Model.HasAuthenticator) |
||||
|
{ |
||||
|
<a asp-page="./EnableAuthenticator" class="btn btn-default">Add authenticator app</a> |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
<a asp-page="./EnableAuthenticator" class="btn btn-default">Configure authenticator app</a> |
||||
|
<a asp-page="./ResetAuthenticator" class="btn btn-default">Reset authenticator app</a> |
||||
|
} |
||||
|
|
||||
|
@section Scripts { |
||||
|
@await Html.PartialAsync("_ValidationScriptsPartial") |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
using Infrastructure.Identity; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.RazorPages.Pages.Account.Manage |
||||
|
{ |
||||
|
public class TwoFactorAuthenticationModel : PageModel |
||||
|
{ |
||||
|
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}"; |
||||
|
|
||||
|
private readonly UserManager<ApplicationUser> _userManager; |
||||
|
private readonly SignInManager<ApplicationUser> _signInManager; |
||||
|
private readonly ILogger<TwoFactorAuthenticationModel> _logger; |
||||
|
|
||||
|
public TwoFactorAuthenticationModel( |
||||
|
UserManager<ApplicationUser> userManager, |
||||
|
SignInManager<ApplicationUser> signInManager, |
||||
|
ILogger<TwoFactorAuthenticationModel> logger) |
||||
|
{ |
||||
|
_userManager = userManager; |
||||
|
_signInManager = signInManager; |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
public bool HasAuthenticator { get; set; } |
||||
|
|
||||
|
public int RecoveryCodesLeft { get; set; } |
||||
|
|
||||
|
[BindProperty] |
||||
|
public bool Is2faEnabled { get; set; } |
||||
|
|
||||
|
public async Task<IActionResult> OnGet() |
||||
|
{ |
||||
|
var user = await _userManager.GetUserAsync(User); |
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); |
||||
|
} |
||||
|
|
||||
|
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null; |
||||
|
Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user); |
||||
|
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user); |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
@{ |
||||
|
Layout = "/Pages/_Layout.cshtml"; |
||||
|
} |
||||
|
|
||||
|
<h2>Manage your account</h2> |
||||
|
|
||||
|
<div> |
||||
|
<h4>Change your account settings</h4> |
||||
|
<hr /> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-3"> |
||||
|
@await Html.PartialAsync("_ManageNav") |
||||
|
</div> |
||||
|
<div class="col-md-9"> |
||||
|
@RenderBody() |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
@section Scripts { |
||||
|
@RenderSection("Scripts", required: false) |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,6 @@ |
|||||
|
<ul class="nav nav-pills nav-stacked"> |
||||
|
<li class="@ManageNavPages.IndexNavClass(ViewContext)"><a asp-page="./Index">Profile</a></li> |
||||
|
<li class="@ManageNavPages.ChangePasswordNavClass(ViewContext)"><a asp-page="./ChangePassword">Password</a></li> |
||||
|
<li class="@ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)"><a asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li> |
||||
|
</ul> |
||||
|
|
||||
@ -0,0 +1,10 @@ |
|||||
|
@model string |
||||
|
|
||||
|
@if (!String.IsNullOrEmpty(Model)) |
||||
|
{ |
||||
|
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; |
||||
|
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert"> |
||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button> |
||||
|
@Model |
||||
|
</div> |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
@using Microsoft.eShopWeb.RazorPages.Pages.Account.Manage |
||||
@ -1,6 +0,0 @@ |
|||||
@page |
|
||||
@model SignoutModel |
|
||||
@{ |
|
||||
ViewData["Title"] = "Signing out"; |
|
||||
} |
|
||||
<h2>Signing out...</h2> |
|
||||
@ -1,32 +0,0 @@ |
|||||
using System.Threading.Tasks; |
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages; |
|
||||
using Microsoft.AspNetCore.Identity; |
|
||||
using Infrastructure.Identity; |
|
||||
using Microsoft.AspNetCore.Mvc; |
|
||||
|
|
||||
namespace Microsoft.eShopWeb.RazorPages.Pages.Account |
|
||||
{ |
|
||||
public class SignoutModel : PageModel |
|
||||
{ |
|
||||
private readonly SignInManager<ApplicationUser> _signInManager; |
|
||||
|
|
||||
public SignoutModel(SignInManager<ApplicationUser> signInManager) |
|
||||
{ |
|
||||
_signInManager = signInManager; |
|
||||
} |
|
||||
|
|
||||
public async Task<IActionResult> OnGet() |
|
||||
{ |
|
||||
await _signInManager.SignOutAsync(); |
|
||||
|
|
||||
return RedirectToPage("/Index"); |
|
||||
} |
|
||||
|
|
||||
public async Task<IActionResult> OnPost() |
|
||||
{ |
|
||||
await _signInManager.SignOutAsync(); |
|
||||
|
|
||||
return RedirectToPage("/Index"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,4 +1,6 @@ |
|||||
@using Microsoft.eShopWeb.RazorPages |
@using Microsoft.eShopWeb.RazorPages |
||||
@using Microsoft.eShopWeb.RazorPages.ViewModels |
@using Microsoft.eShopWeb.RazorPages.ViewModels |
||||
|
@using Microsoft.AspNetCore.Identity |
||||
|
@using Infrastructure.Identity |
||||
@namespace Microsoft.eShopWeb.RazorPages.Pages |
@namespace Microsoft.eShopWeb.RazorPages.Pages |
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
||||
|
|||||
Loading…
Reference in new issue