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; |
|||
|
|||
namespace Microsoft.eShopWeb.ViewModels |
|||
namespace Microsoft.eShopWeb.ViewModels.Account |
|||
{ |
|||
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.ComponentModel.DataAnnotations; |
|||
|
|||
namespace Microsoft.eShopWeb.ViewModels |
|||
namespace Microsoft.eShopWeb.ViewModels.Account |
|||
{ |
|||
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.ViewModels |
|||
@using Microsoft.eShopWeb.ViewModels.Account |
|||
@using Microsoft.eShopWeb.ViewModels.Manage |
|||
@using Microsoft.AspNetCore.Identity |
|||
@using Infrastructure.Identity |
|||
@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.ViewModels |
|||
@using Microsoft.AspNetCore.Identity |
|||
@using Infrastructure.Identity |
|||
@namespace Microsoft.eShopWeb.RazorPages.Pages |
|||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
|||
|
|||
Loading…
Reference in new issue