Split showing and generating recovery codes

This commit is contained in:
Pranav K 2017-12-06 13:55:51 -08:00
parent e4ecd070eb
commit 16c23b846e
12 changed files with 170 additions and 40 deletions

View File

@ -5,6 +5,6 @@
} }
<header> <header>
<h1 class="text-danger">ViewData["Title"]</h1> <h1 class="text-danger">@ViewData["Title"]</h1>
<p class="text-danger">You do not have access to this resource.</p> <p class="text-danger">You do not have access to this resource.</p>
</header> </header>

View File

@ -20,7 +20,7 @@ namespace Company.WebApplication1.Pages.Account.Manage
private readonly ILogger<EnableAuthenticatorModel> _logger; private readonly ILogger<EnableAuthenticatorModel> _logger;
private readonly UrlEncoder _urlEncoder; 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( public EnableAuthenticatorModel(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
@ -90,7 +90,10 @@ namespace Company.WebApplication1.Pages.Account.Manage
await _userManager.SetTwoFactorEnabledAsync(user, true); await _userManager.SetTwoFactorEnabledAsync(user, true);
_logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", user.Id); _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) private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user)
@ -127,7 +130,7 @@ namespace Company.WebApplication1.Pages.Account.Manage
private string GenerateQrCodeUri(string email, string unformattedKey) private string GenerateQrCodeUri(string email, string unformattedKey)
{ {
return string.Format( return string.Format(
AuthenicatorUriFormat, AuthenticatorUriFormat,
_urlEncoder.Encode("Company.WebApplication1"), _urlEncoder.Encode("Company.WebApplication1"),
_urlEncoder.Encode(email), _urlEncoder.Encode(email),
unformattedKey); unformattedKey);

View File

@ -1,7 +1,7 @@
@page @page
@model GenerateRecoveryCodesModel @model GenerateRecoveryCodesModel
@{ @{
ViewData["Title"] = "Recovery codes"; ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes";
ViewData["ActivePage"] = "TwoFactorAuthentication"; ViewData["ActivePage"] = "TwoFactorAuthentication";
} }
@ -9,17 +9,19 @@
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
<p> <p>
<span class="glyphicon glyphicon-warning-sign"></span> <span class="glyphicon glyphicon-warning-sign"></span>
<strong>Put these codes in a safe place.</strong> <strong>This action generates new recovery codes.</strong>
</p> </p>
<p> <p>
If you lose your device and don't have the recovery codes you will lose access to your account. If you lose your device and don't have the recovery codes you will lose access to your account.
</p> </p>
<p>
Generating new recovery codes 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>
<div class="row">
<div class="col-md-12"> <div>
@for (var row = 0; row < Model.RecoveryCodes.Count(); row += 2) <form asp-action="GenerateRecoveryCodes" method="post" class="form-group">
{ <button class="btn btn-danger" type="submit">Generate Recovery Codes</button>
<code>@Model.RecoveryCodes[row]</code><text>&nbsp;</text><code>@Model.RecoveryCodes[row + 1]</code><br /> </form>
}
</div>
</div> </div>

View File

@ -23,8 +23,6 @@ namespace Company.WebApplication1.Pages.Account.Manage
_logger = logger; _logger = logger;
} }
public string[] RecoveryCodes { get; set; }
public async Task<IActionResult> OnGetAsync() public async Task<IActionResult> OnGetAsync()
{ {
var user = await _userManager.GetUserAsync(User); 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)}'."); 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<IActionResult> 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) if (!user.TwoFactorEnabled)
{ {
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); 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 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); _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", user.Id);
return Page(); return RedirectToPage("./ShowRecoveryCodes");
} }
} }
} }

View File

@ -0,0 +1,25 @@
@page
@model ShowRecoveryCodesModel
@{
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.Length; row += 2)
{
<code>@Model.RecoveryCodes[row]</code><text>&nbsp;</text><code>@Model.RecoveryCodes[row + 1]</code><br />
}
</div>
</div>

View File

@ -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();
}
}
}

View File

@ -26,7 +26,8 @@ namespace Company.WebApplication1.Controllers
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder; 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( public ManageController(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
@ -408,7 +409,23 @@ namespace Company.WebApplication1.Controllers
await _userManager.SetTwoFactorEnabledAsync(user, true); await _userManager.SetTwoFactorEnabledAsync(user, true);
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id); _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] [HttpGet]
@ -435,6 +452,24 @@ namespace Company.WebApplication1.Controllers
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> 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<IActionResult> GenerateRecoveryCodes() public async Task<IActionResult> GenerateRecoveryCodes()
{ {
var user = await _userManager.GetUserAsync(User); var user = await _userManager.GetUserAsync(User);
@ -449,11 +484,11 @@ namespace Company.WebApplication1.Controllers
} }
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); 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); _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 #region Helpers
@ -486,7 +521,7 @@ namespace Company.WebApplication1.Controllers
private string GenerateQrCodeUri(string email, string unformattedKey) private string GenerateQrCodeUri(string email, string unformattedKey)
{ {
return string.Format( return string.Format(
AuthenicatorUriFormat, AuthenticatorUriFormat,
_urlEncoder.Encode("Company.WebApplication1"), _urlEncoder.Encode("Company.WebApplication1"),
_urlEncoder.Encode(email), _urlEncoder.Encode(email),
unformattedKey); unformattedKey);

View File

@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace Company.WebApplication1.Models.ManageViewModels namespace Company.WebApplication1.Models.ManageViewModels
{ {
public class GenerateRecoveryCodesViewModel public class ShowRecoveryCodesViewModel
{ {
public string[] RecoveryCodes { get; set; } public string[] RecoveryCodes { get; set; }
} }

View File

@ -3,6 +3,6 @@
} }
<header> <header>
<h2 class="text-danger">ViewData["Title"]</h2> <h2 class="text-danger">@ViewData["Title"]</h2>
<p class="text-danger">You do not have access to this resource.</p> <p class="text-danger">You do not have access to this resource.</p>
</header> </header>

View File

@ -1,24 +1,26 @@
@model GenerateRecoveryCodesViewModel @{
@{ ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes";
ViewData["Title"] = "Recovery codes";
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
} }
<h4>@ViewData["Title"]</h4> <h2>@ViewData["Title"]</h2>
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
<p> <p>
<span class="glyphicon glyphicon-warning-sign"></span> <span class="glyphicon glyphicon-warning-sign"></span>
<strong>Put these codes in a safe place.</strong> <strong>This action generates new recovery codes.</strong>
</p> </p>
<p> <p>
If you lose your device and don't have the recovery codes you will lose access to your account. If you lose your device and don't have the recovery codes you will lose access to your account.
</p> </p>
<p>
Generating new recovery codes 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>
<div class="row">
<div class="col-md-12"> <div>
@for (var row = 0; row < Model.RecoveryCodes.Count(); row += 2) <form asp-action="GenerateRecoveryCodes" method="post" class="form-group">
{ <button class="btn btn-danger" type="submit">Generate Recovery Codes</button>
<code>@Model.RecoveryCodes[row]</code><text>&nbsp;</text><code>@Model.RecoveryCodes[row + 1]</code><br /> </form>
}
</div>
</div> </div>

View File

@ -0,0 +1,24 @@
@model ShowRecoveryCodesViewModel
@{
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.Length; row += 2)
{
<code>@Model.RecoveryCodes[row]</code><text>&nbsp;</text><code>@Model.RecoveryCodes[row + 1]</code><br />
}
</div>
</div>

View File

@ -30,7 +30,7 @@
} }
<a asp-action="Disable2faWarning" class="btn btn-default">Disable 2FA</a> <a asp-action="Disable2faWarning" class="btn btn-default">Disable 2FA</a>
<a asp-action="GenerateRecoveryCodes" class="btn btn-default">Reset recovery codes</a> <a asp-action="GenerateRecoveryCodesWarning" class="btn btn-default">Reset recovery codes</a>
} }
<h5>Authenticator app</h5> <h5>Authenticator app</h5>