Unify Fido2 authentication under two-factor tab (#2866)

* Unify Fido2 authentication under two-factor tab

Closes #2754.

* Improve UI and wording

* Improve register FIDO2 device page
This commit is contained in:
d11n 2021-09-13 03:16:52 +02:00 committed by GitHub
parent eccbe8e018
commit 6666786b7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 161 additions and 166 deletions

View File

@ -24,7 +24,6 @@ namespace BTCPayServer.Data
.HasOne(o => o.ApplicationUser)
.WithMany(i => i.Fido2Credentials)
.HasForeignKey(i => i.ApplicationUserId).OnDelete(DeleteBehavior.Cascade);
}
public ApplicationUser ApplicationUser { get; set; }

View File

@ -241,7 +241,6 @@ namespace BTCPayServer.Tests
{
var links = Driver.FindElements(By.CssSelector(".nav .nav-link")).Select(c => c.GetAttribute("href")).ToList();
Driver.AssertNoError();
Assert.NotEmpty(links);
foreach (var l in links)
{
Logs.Tester.LogInformation($"Checking no error on {l}");

View File

@ -28,6 +28,7 @@ using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.ManageViewModels;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
@ -3537,14 +3538,15 @@ namespace BTCPayServer.Tests
Password = user.RegisterDetails.Password
})).ActionName);
var listController = user.GetController<ManageController>();
var manageController = user.GetController<Fido2Controller>();
//by default no fido2 devices available
Assert.Empty(Assert
.IsType<Fido2AuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.List()).Model).Credentials);
.IsType<TwoFactorAuthenticationViewModel>(Assert
.IsType<ViewResult>(await listController.TwoFactorAuthentication()).Model).Credentials);
Assert.IsType<CredentialCreateOptions>(Assert
.IsType<ViewResult>(await manageController.Create(new AddFido2CredentialViewModel()
.IsType<ViewResult>(await manageController.Create(new AddFido2CredentialViewModel
{
Name = "label"
})).Model);
@ -3572,8 +3574,8 @@ namespace BTCPayServer.Tests
Assert.NotNull(newDevice.Id);
Assert.NotEmpty(Assert
.IsType<Fido2AuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.List()).Model).Credentials);
.IsType<TwoFactorAuthenticationViewModel>(Assert
.IsType<ViewResult>(await listController.TwoFactorAuthentication()).Model).Credentials);
}
//check if we are showing the fido2 login screen now

View File

@ -29,6 +29,7 @@ namespace BTCPayServer.Controllers
{
Is2faEnabled = user.TwoFactorEnabled,
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
Credentials = await _fido2Service.GetCredentials( _userManager.GetUserId(User))
};
return View(model);

View File

@ -3,6 +3,7 @@ using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Data;
using BTCPayServer.Fido2;
using BTCPayServer.Models.ManageViewModels;
using BTCPayServer.Security.GreenField;
using BTCPayServer.Services;
@ -30,11 +31,10 @@ namespace BTCPayServer.Controllers
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly APIKeyRepository _apiKeyRepository;
private readonly IAuthorizationService _authorizationService;
private readonly Fido2Service _fido2Service;
private readonly LinkGenerator _linkGenerator;
readonly StoreRepository _StoreRepository;
public ManageController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
@ -47,6 +47,7 @@ namespace BTCPayServer.Controllers
BTCPayServerEnvironment btcPayServerEnvironment,
APIKeyRepository apiKeyRepository,
IAuthorizationService authorizationService,
Fido2Service fido2Service,
LinkGenerator linkGenerator
)
{
@ -58,6 +59,7 @@ namespace BTCPayServer.Controllers
_btcPayServerEnvironment = btcPayServerEnvironment;
_apiKeyRepository = apiKeyRepository;
_authorizationService = authorizationService;
_fido2Service = fido2Service;
_linkGenerator = linkGenerator;
_StoreRepository = storeRepository;
}

View File

@ -12,7 +12,6 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Fido2
{
[Route("fido2")]
[Authorize]
public class Fido2Controller : Controller
@ -26,34 +25,24 @@ namespace BTCPayServer.Fido2
_fido2Service = fido2Service;
}
[HttpGet("")]
public async Task<IActionResult> List()
{
return View(new Fido2AuthenticationViewModel()
{
Credentials = await _fido2Service.GetCredentials( _userManager.GetUserId(User))
});
}
[HttpGet("{id}/delete")]
public IActionResult Remove(string id)
{
return View("Confirm", new ConfirmModel("Are you sure you want to remove FIDO2 credential?", "Your account will no longer have this credential as an option for MFA.", "Remove"));
return View("Confirm", new ConfirmModel("Remove security device", "Your account will no longer have this security device as an option for two-factor authentication.", "Remove"));
}
[HttpPost("{id}/delete")]
public async Task<IActionResult> RemoveP(string id)
{
await _fido2Service.Remove(id, _userManager.GetUserId(User));
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"FIDO2 Credentials were removed successfully."
Html = "The security device was removed successfully."
});
return RedirectToAction(nameof(List));
return RedirectToList();
}
[HttpGet("register")]
@ -65,10 +54,10 @@ namespace BTCPayServer.Fido2
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"FIDO2 Credentials could not be saved."
Html = "The security device could not be registered."
});
return RedirectToAction(nameof(List));
return RedirectToList();
}
ViewData["CredentialName"] = viewModel.Name ?? "";
@ -84,7 +73,7 @@ namespace BTCPayServer.Fido2
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"FIDO2 Credentials were saved successfully."
Html = "The security device was registered successfully."
});
}
else
@ -92,12 +81,16 @@ namespace BTCPayServer.Fido2
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"FIDO2 Credentials could not be saved."
Html = "The security device could not be registered."
});
}
return RedirectToAction(nameof(List));
return RedirectToList();
}
private ActionResult RedirectToList()
{
return RedirectToAction("TwoFactorAuthentication", "Manage");
}
}
}

View File

@ -1,3 +1,6 @@
using System.Collections.Generic;
using BTCPayServer.Data;
namespace BTCPayServer.Models.ManageViewModels
{
public class TwoFactorAuthenticationViewModel
@ -6,5 +9,7 @@ namespace BTCPayServer.Models.ManageViewModels
public int RecoveryCodesLeft { get; set; }
public bool Is2faEnabled { get; set; }
public List<Fido2Credential> Credentials { get; set; }
}
}

View File

@ -1,23 +1,25 @@
@model Fido2NetLib.CredentialCreateOptions
@{
ViewData.SetActivePageAndTitle(ManageNavPages.Fido2, "Register FIDO2 Credentials");
ViewData.SetActivePageAndTitle(ManageNavPages.Fido2, "Register your security device");
}
<form asp-action="CreateResponse" id="registerForm">
<input type="hidden" name="data" id="data" />
<input type="hidden" name="name" id="name" value="@(ViewData.ContainsKey("CredentialName")? ViewData["CredentialName"] : string.Empty)" />
</form>
<div class="row">
<div class="col-lg-12 section-heading">
<div>
<span id="spinner" class="fa fa-spinner fa-spin float-end ms-3 me-5 mt-1 fido-running" style="font-size:2.5em"></span>
<p>Insert your security key into your computer's USB port. If it has a button, tap on it.</p>
<div class="col-lg-8">
<div id="info-message" class="alert alert-info my-3 d-flex justify-content-center align-items-center">
<span id="spinner" class="fa fa-spinner fa-spin float-end me-3 fido-running" style="font-size:2.5em"></span>
<span>Insert your security device into your computer's USB port. If it has a button, tap on it.</span>
</div>
<p id="error-message" class="d-none alert alert-danger"></p>
<a id="btn-retry" class="btn btn-secondary d-none" href="javascript:window.location.reload()" csp-allow>Retry</a>
<a id="btn-retry" class="btn btn-secondary d-none">Retry</a>
</div>
</div>
<script>
document.getElementById('btn-retry').addEventListener('click', function () { window.location.reload() });
// send to server for registering
window.makeCredentialOptions = @Json.Serialize(Model);
</script>

View File

@ -1,49 +0,0 @@
@model BTCPayServer.Fido2.Models.Fido2AuthenticationViewModel
@{
ViewData.SetActivePageAndTitle(ManageNavPages.Fido2, "Registered FIDO2 Credentials");
}
<table class="table table-lg mb-4">
<thead>
<tr>
<th>Name</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var device in Model.Credentials)
{
var name = string.IsNullOrEmpty(device.Name) ? "Unnamed FIDO2 credential" : device.Name;
<tr>
<td>@name</td>
<td class="text-end">
<a asp-action="Remove" asp-route-id="@device.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="Your account will no longer have the credential <strong>@name</strong> as an option for multi-factor authentication." data-confirm-input="REMOVE">Remove</a>
</td>
</tr>
}
@if (!Model.Credentials.Any())
{
<tr>
<td colspan="2" class="text-center h5 py-2">
No registered credentials
</td>
</tr>
}
</tbody>
</table>
<form asp-action="Create" method="get">
<div class="row g-1">
<div class="col">
<input type="text" class="form-control" name="Name" placeholder="New Credential Name"/>
</div>
<div class="col">
<button type="submit" class="btn btn-primary ms-2">
<span class="fa fa-plus"></span>
Add New Credential
</button>
</div>
</div>
</form>
<partial name="_Confirm" model="@(new ConfirmModel("Remove FIDO2 credential", "Your account will no longer have the credential as an option for multi-factor authentication.", "Remove"))" />

View File

@ -22,7 +22,7 @@
</ul>
</li>
<li class="mb-5">
<p class="mb-2">Scan the QR Code or enter the following key into your two factor authenticator app:</p>
<p class="mb-2">Scan the QR Code or enter the following key into your two-factor authenticator app:</p>
<p class="mb-4">
<code class="me-3">@Model.SharedKey</code>
<span class="text-secondary">(spaces and casing do not matter)</span>
@ -32,7 +32,7 @@
</li>
<li>
<p>
Your two factor authenticator app will provide you with a unique code.
Your two-factor authenticator app will provide you with a unique code.
<br/>
Enter the code in the confirmation box below.
</p>

View File

@ -3,6 +3,18 @@
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Two-factor authentication");
}
<div class="row">
<div class="col-lg-10 col-xl-8">
<p>
Two-factor authentication (2FA) is an additional measure to protect your account.
In addition to your password you will be asked for a second proof on login.
This can be provided by an app (such as Google or Microsoft Authenticator)
or a security device (like a Yubikey or your hardware wallet supporting FIDO2).
</p>
<h4 class="mb-3">App-based 2FA</h4>
@if (Model.Is2faEnabled)
{
if (Model.RecoveryCodesLeft == 0)
@ -37,33 +49,33 @@
}
}
<div class="list-group">
<div class="list-group mb-3">
@if (Model.Is2faEnabled)
{
<a asp-action="Disable2fa" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Disable two-factor authentication (2FA)" data-description="Disabling 2FA does not change the keys used in the authenticator apps. If you wish to change the key used in an authenticator app you should reset your authenticator keys." data-confirm="Disable 2FA">
<a asp-action="Disable2fa" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Disable two-factor authentication (2FA)" data-description="Disabling 2FA does not change the keys used in the authenticator apps. If you wish to change the key used in an authenticator app you should reset your authenticator keys." data-confirm="Disable" data-confirm-input="DISABLE">
<div>
<h5>Disable 2FA</h5>
<p class="mb-0 me-3">Disable two-factor authentication. Re-enabling will not require you to reconfigure your Authenticator app. </p>
<p class="mb-0 me-3">Re-enabling will not require you to reconfigure your app.</p>
</div>
<vc:icon symbol="caret-right" />
</a>
<a asp-action="GenerateRecoveryCodes" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Reset recovery codes" data-description="Your existing recovery codes will no longer be valid!" data-confirm="Reset">
<a asp-action="GenerateRecoveryCodes" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Reset recovery codes" data-description="Your existing recovery codes will no longer be valid!" data-confirm="Reset" data-confirm-input="RESET">
<div>
<h5>Reset recovery codes</h5>
<p class="mb-0 me-3">Regenerate your two-factor recovery codes.</p>
<p class="mb-0 me-3">Regenerate your 2FA recovery codes.</p>
</div>
<vc:icon symbol="caret-right" />
</a>
<a asp-action="ResetAuthenticator" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Reset authenticator app" data-description="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." data-confirm="Reset">
<a asp-action="ResetAuthenticator" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Reset authenticator app" data-description="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." data-confirm="Reset" data-confirm-input="RESET">
<div>
<h5>Reset authenticator app</h5>
<h5>Reset app</h5>
<p class="mb-0 me-3">Invalidates the current authenticator configuration. Useful if you believe your authenticator settings were compromised.</p>
</div>
<vc:icon symbol="caret-right" />
</a>
<a asp-action="EnableAuthenticator" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3">
<div>
<h5>Configure Authenticator app</h5>
<h5>Configure app</h5>
<p class="mb-0 me-3">Display the key or QR code to configure an authenticator app with your current setup.</p>
</div>
<vc:icon symbol="caret-right" />
@ -74,12 +86,40 @@
<a asp-action="EnableAuthenticator" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3">
<div>
<h5>Enable 2FA</h5>
<p class="mb-0 me-3">Enable two-factor authentication using TOTP with apps such as Google Authenticator.</p>
<p class="mb-0 me-3">Using apps such as Google or Microsoft Authenticator.</p>
</div>
<vc:icon symbol="caret-right" />
</a>
}
</div>
<partial name="_Confirm" model="@(new ConfirmModel("Remove 2FA credential", "Your account will no longer have the credential as an option for multi-factor authentication.", "Remove"))" />
<h4 class="mt-4 mb-3">Security devices</h4>
@if (Model.Credentials.Any())
{
<div class="list-group mb-3">
@foreach (var device in Model.Credentials)
{
var name = string.IsNullOrEmpty(device.Name) ? "Unnamed security device" : device.Name;
<div class="list-group-item d-flex justify-content-between align-items-center py-3">
<h5 class="mb-0">@name</h5>
<a asp-controller="Fido2" asp-action="Remove" asp-route-id="@device.Id" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Remove security device" data-description="Your account will no longer have the security device <strong>@name</strong> as an option for two-factor authentication." data-confirm="Remove" data-confirm-input="REMOVE">Remove</a>
</div>
}
</div>
}
<form asp-controller="Fido2" asp-action="Create" method="get">
<div class="input-group">
<input type="text" class="form-control" name="Name" placeholder="Security device name"/>
<button type="submit" class="btn btn-primary">
<span class="fa fa-plus"></span>
Add
<span class="d-none d-md-inline-block">security device</span>
</button>
</div>
</form>
</div>
</div>
<partial name="_Confirm" model="@(new ConfirmModel("Two-Factor Authentication", "Placeholder", "Placeholder"))" />

View File

@ -3,7 +3,6 @@
<a id="@ManageNavPages.Index.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-controller="Manage" asp-action="Index">Profile</a>
<a id="@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-controller="Manage" asp-action="ChangePassword">Password</a>
<a id="@ManageNavPages.TwoFactorAuthentication.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-controller="Manage" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
<a id="@ManageNavPages.Fido2.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Fido2)" asp-action="List" asp-controller="Fido2">FIDO2 Authentication</a>
<a id="@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-controller="Manage" asp-action="APIKeys">API Keys</a>
<a id="@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-controller="Manage" asp-action="NotificationSettings">Notifications</a>
<vc:ui-extension-point location="user-nav"/>

View File

@ -73,6 +73,8 @@ function showErrorAlert(message, error) {
footermsg = 'exception:' + error.toString();
}
console.error(message, footermsg);
document.getElementById("info-message").classList.add("d-none");
document.getElementById("btn-retry").classList.remove("d-none");
document.getElementById("error-message").textContent = message;
for(let el of document.getElementsByClassName("fido-running")){