Adding 2FA Authenticator Support (#66)
* 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.
This commit is contained in:
41
src/WebRazorPages/Pages/Account/LoginWith2fa.cshtml
Normal file
41
src/WebRazorPages/Pages/Account/LoginWith2fa.cshtml
Normal file
@@ -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")
|
||||
}
|
||||
94
src/WebRazorPages/Pages/Account/LoginWith2fa.cshtml.cs
Normal file
94
src/WebRazorPages/Pages/Account/LoginWith2fa.cshtml.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/WebRazorPages/Pages/Account/LoginWithRecoveryCode.cshtml
Normal file
29
src/WebRazorPages/Pages/Account/LoginWithRecoveryCode.cshtml
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/WebRazorPages/Pages/Account/Manage/ChangePassword.cshtml
Normal file
35
src/WebRazorPages/Pages/Account/Manage/ChangePassword.cshtml
Normal file
@@ -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")
|
||||
}
|
||||
100
src/WebRazorPages/Pages/Account/Manage/ChangePassword.cshtml.cs
Normal file
100
src/WebRazorPages/Pages/Account/Manage/ChangePassword.cshtml.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/WebRazorPages/Pages/Account/Manage/Disable2fa.cshtml
Normal file
25
src/WebRazorPages/Pages/Account/Manage/Disable2fa.cshtml
Normal file
@@ -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>
|
||||
59
src/WebRazorPages/Pages/Account/Manage/Disable2fa.cshtml.cs
Normal file
59
src/WebRazorPages/Pages/Account/Manage/Disable2fa.cshtml.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/WebRazorPages/Pages/Account/Manage/Index.cshtml
Normal file
45
src/WebRazorPages/Pages/Account/Manage/Index.cshtml
Normal file
@@ -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")
|
||||
}
|
||||
125
src/WebRazorPages/Pages/Account/Manage/Index.cshtml.cs
Normal file
125
src/WebRazorPages/Pages/Account/Manage/Index.cshtml.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/WebRazorPages/Pages/Account/Manage/ManageNavPages.cs
Normal file
31
src/WebRazorPages/Pages/Account/Manage/ManageNavPages.cs
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/WebRazorPages/Pages/Account/Manage/SetPassword.cshtml
Normal file
35
src/WebRazorPages/Pages/Account/Manage/SetPassword.cshtml
Normal file
@@ -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")
|
||||
}
|
||||
91
src/WebRazorPages/Pages/Account/Manage/SetPassword.cshtml.cs
Normal file
91
src/WebRazorPages/Pages/Account/Manage/SetPassword.cshtml.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/WebRazorPages/Pages/Account/Manage/_Layout.cshtml
Normal file
23
src/WebRazorPages/Pages/Account/Manage/_Layout.cshtml
Normal file
@@ -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)
|
||||
}
|
||||
|
||||
6
src/WebRazorPages/Pages/Account/Manage/_ManageNav.cshtml
Normal file
6
src/WebRazorPages/Pages/Account/Manage/_ManageNav.cshtml
Normal file
@@ -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>
|
||||
|
||||
10
src/WebRazorPages/Pages/Account/Manage/_StatusMessage.cshtml
Normal file
10
src/WebRazorPages/Pages/Account/Manage/_StatusMessage.cshtml
Normal file
@@ -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,10 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
using Infrastructure.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.eShopWeb.RazorPages.ViewModels;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Infrastructure.Identity;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.eShopWeb.RazorPages.Pages.Account
|
||||
{
|
||||
|
||||
@@ -71,6 +71,10 @@ namespace Microsoft.eShopWeb.RazorPages.Pages.Account
|
||||
}
|
||||
return RedirectToPage(returnUrl ?? "/Index");
|
||||
}
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = LoginDetails.RememberMe });
|
||||
}
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return Page();
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
<section class="col-lg-4 col-md-5 col-xs-12">
|
||||
<div class="esh-identity">
|
||||
<form asp-page="/Account/Signout" method="post"
|
||||
<form asp-controller="Account" asp-action="Logout" method="post"
|
||||
id="logoutForm" class="navbar-right">
|
||||
<section class="esh-identity-section">
|
||||
@*<div class="esh-identity-name">@User.FindFirst(x => x.Type == "preferred_username").Value</div>*@
|
||||
@@ -19,6 +19,14 @@
|
||||
<img class="esh-identity-image" src="~/images/my_orders.png">
|
||||
</a>
|
||||
|
||||
<a class="esh-identity-item"
|
||||
asp-page="/Account/Manage/Index">
|
||||
|
||||
<div class="esh-identity-name esh-identity-name--upper">My account</div>
|
||||
<img class="esh-identity-image" src="~/images/my_orders.png">
|
||||
</a>
|
||||
|
||||
|
||||
<a class="esh-identity-item"
|
||||
href="javascript:document.getElementById('logoutForm').submit()">
|
||||
<div class="esh-identity-name esh-identity-name--upper">Log Out</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user