diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/AccessDenied.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/AccessDenied.cshtml index ba7d0215dc..3510edef22 100644 --- a/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/AccessDenied.cshtml +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/AccessDenied.cshtml @@ -5,6 +5,6 @@ }
-

ViewData["Title"]

+

@ViewData["Title"]

You do not have access to this resource.

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/EnableAuthenticator.cshtml.cs index f23f1431f7..8f38fb8492 100644 --- a/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/EnableAuthenticator.cshtml.cs +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -20,7 +20,7 @@ namespace Company.WebApplication1.Pages.Account.Manage private readonly ILogger _logger; private readonly UrlEncoder _urlEncoder; - private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; public EnableAuthenticatorModel( UserManager userManager, @@ -90,7 +90,10 @@ namespace Company.WebApplication1.Pages.Account.Manage await _userManager.SetTwoFactorEnabledAsync(user, true); _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", user.Id); - return RedirectToPage("./GenerateRecoveryCodes"); + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + TempData["RecoveryCodes"] = recoveryCodes.ToArray(); + return RedirectToPage("./ShowRecoveryCodes"); } private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user) @@ -127,7 +130,7 @@ namespace Company.WebApplication1.Pages.Account.Manage private string GenerateQrCodeUri(string email, string unformattedKey) { return string.Format( - AuthenicatorUriFormat, + AuthenticatorUriFormat, _urlEncoder.Encode("Company.WebApplication1"), _urlEncoder.Encode(email), unformattedKey); diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml index d05825429f..3738b9237c 100644 --- a/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml @@ -1,7 +1,7 @@ @page @model GenerateRecoveryCodesModel @{ - ViewData["Title"] = "Recovery codes"; + ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; ViewData["ActivePage"] = "TwoFactorAuthentication"; } @@ -9,17 +9,19 @@ + +
+
+ +
-
-
- @for (var row = 0; row < Model.RecoveryCodes.Count(); row += 2) - { - @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
- } -
-
\ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs index 6839d275fc..6c23b5ad60 100644 --- a/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs @@ -23,8 +23,6 @@ namespace Company.WebApplication1.Pages.Account.Manage _logger = logger; } - public string[] RecoveryCodes { get; set; } - public async Task OnGetAsync() { var user = await _userManager.GetUserAsync(User); @@ -33,17 +31,33 @@ namespace Company.WebApplication1.Pages.Account.Manage 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}' because they do not have 2FA enabled."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + 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(); + TempData["RecoveryCodes"] = recoveryCodes.ToArray(); _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", user.Id); - return Page(); + return RedirectToPage("./ShowRecoveryCodes"); } } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ShowRecoveryCodes.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ShowRecoveryCodes.cshtml new file mode 100644 index 0000000000..a7225dfa23 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ShowRecoveryCodes.cshtml @@ -0,0 +1,25 @@ +@page +@model ShowRecoveryCodesModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData["ActivePage"] = "TwoFactorAuthentication"; +} + +

@ViewData["Title"]

+ +
+
+ @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ } +
+
\ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs new file mode 100644 index 0000000000..250bf7f598 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Company.WebApplication1.Pages.Account.Manage +{ + public class ShowRecoveryCodesModel : PageModel + { + public string[] RecoveryCodes { get; private set; } + + public IActionResult OnGet() + { + RecoveryCodes = (string[])TempData["RecoveryCodes"]; + if (RecoveryCodes == null) + { + return RedirectToPage("TwoFactorAuthentication"); + } + + return Page(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Controllers/ManageController.cs b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Controllers/ManageController.cs index 437e01129e..1620109d56 100644 --- a/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Controllers/ManageController.cs +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Controllers/ManageController.cs @@ -26,7 +26,8 @@ namespace Company.WebApplication1.Controllers private readonly ILogger _logger; private readonly UrlEncoder _urlEncoder; - private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + private const string RecoveryCodesKey = nameof(RecoveryCodesKey); public ManageController( UserManager userManager, @@ -408,7 +409,23 @@ namespace Company.WebApplication1.Controllers await _userManager.SetTwoFactorEnabledAsync(user, true); _logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id); - return RedirectToAction(nameof(GenerateRecoveryCodes)); + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + TempData[RecoveryCodesKey] = recoveryCodes.ToArray(); + + return RedirectToAction(nameof(ShowRecoveryCodes)); + } + + [HttpGet] + public IActionResult ShowRecoveryCodes() + { + var recoveryCodes = (string[])TempData[RecoveryCodesKey]; + if (recoveryCodes == null) + { + return RedirectToAction(nameof(TwoFactorAuthentication)); + } + + var model = new ShowRecoveryCodesViewModel { RecoveryCodes = recoveryCodes }; + return View(model); } [HttpGet] @@ -435,6 +452,24 @@ namespace Company.WebApplication1.Controllers } [HttpGet] + public async Task GenerateRecoveryCodesWarning() + { + 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}' because they do not have 2FA enabled."); + } + + return View(nameof(GenerateRecoveryCodes)); + } + + [HttpPost] + [ValidateAntiForgeryToken] public async Task GenerateRecoveryCodes() { var user = await _userManager.GetUserAsync(User); @@ -449,11 +484,11 @@ namespace Company.WebApplication1.Controllers } 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); + var model = new ShowRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() }; + + return View(nameof(ShowRecoveryCodes), model); } #region Helpers @@ -486,7 +521,7 @@ namespace Company.WebApplication1.Controllers private string GenerateQrCodeUri(string email, string unformattedKey) { return string.Format( - AuthenicatorUriFormat, + AuthenticatorUriFormat, _urlEncoder.Encode("Company.WebApplication1"), _urlEncoder.Encode(email), unformattedKey); diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Models/ManageViewModels/ShowRecoveryCodesViewModel.cs similarity index 85% rename from src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs rename to src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Models/ManageViewModels/ShowRecoveryCodesViewModel.cs index 05f1fe8a70..d026507079 100644 --- a/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Models/ManageViewModels/ShowRecoveryCodesViewModel.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Company.WebApplication1.Models.ManageViewModels { - public class GenerateRecoveryCodesViewModel + public class ShowRecoveryCodesViewModel { public string[] RecoveryCodes { get; set; } } diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Account/AccessDenied.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Account/AccessDenied.cshtml index 3a5a00852c..c27d3daeb3 100644 --- a/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Account/AccessDenied.cshtml +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Account/AccessDenied.cshtml @@ -3,6 +3,6 @@ }
-

ViewData["Title"]

+

@ViewData["Title"]

You do not have access to this resource.

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Manage/GenerateRecoveryCodes.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Manage/GenerateRecoveryCodes.cshtml index 669d13ef93..996967b3a2 100644 --- a/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Manage/GenerateRecoveryCodes.cshtml +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Manage/GenerateRecoveryCodes.cshtml @@ -1,24 +1,26 @@ -@model GenerateRecoveryCodesViewModel -@{ - ViewData["Title"] = "Recovery codes"; +@{ + ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); } -

@ViewData["Title"]

+

@ViewData["Title"]

+ + +
+
+ +
-
-
- @for (var row = 0; row < Model.RecoveryCodes.Count(); row += 2) - { - @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
- } -
-
\ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Manage/ShowRecoveryCodes.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Manage/ShowRecoveryCodes.cshtml new file mode 100644 index 0000000000..c2be067710 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Manage/ShowRecoveryCodes.cshtml @@ -0,0 +1,24 @@ +@model ShowRecoveryCodesViewModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

@ViewData["Title"]

+ +
+
+ @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ } +
+
\ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Manage/TwoFactorAuthentication.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Manage/TwoFactorAuthentication.cshtml index a2b52ac5b4..9286c0821e 100644 --- a/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Manage/TwoFactorAuthentication.cshtml +++ b/src/Microsoft.DotNet.Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Manage/TwoFactorAuthentication.cshtml @@ -30,7 +30,7 @@ } Disable 2FA - Reset recovery codes + Reset recovery codes }
Authenticator app