FIDO2/WebAuthN Support (#2356)

* FIDO2/WebAuthN Support

This adds initial support for WebAuthN/FIDO2 as another MFA mode. U2F is still intact and runs alongside it for now. Once this is merged, I will start work on migrating U2F support to happen over the FIDO2 protocol instead.

* Refactor and future proof system (prep work of seamless u2f migration)

* attempt js fix for mobile devices

* Apply suggestions from code review

Co-authored-by: d11n <mail@dennisreimann.de>

* fix fido name saving

* do not spam logs and hide loader when failed

* PR Changes

* Apply suggestions from code review

Co-authored-by: d11n <mail@dennisreimann.de>

* attempt fido2 bump

* add name if not named for credentials

Co-authored-by: d11n <mail@dennisreimann.de>
This commit is contained in:
Andrew Camilleri
2021-04-20 07:06:32 +02:00
committed by GitHub
parent 315284d5f5
commit 0554565b30
30 changed files with 1004 additions and 25 deletions

View File

@@ -1,24 +1,25 @@
using System;
using System.Globalization;
using System.Security.Policy;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Fido2;
using BTCPayServer.Fido2.Models;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.U2F;
using BTCPayServer.U2F.Models;
using Fido2NetLib;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using NicolasDorier.RateLimits;
using U2F.Core.Exceptions;
@@ -35,7 +36,7 @@ namespace BTCPayServer.Controllers
readonly Configuration.BTCPayServerOptions _Options;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
public U2FService _u2FService;
private readonly RateLimitService _rateLimitService;
private readonly Fido2Service _fido2Service;
private readonly EventAggregator _eventAggregator;
readonly ILogger _logger;
@@ -47,8 +48,8 @@ namespace BTCPayServer.Controllers
Configuration.BTCPayServerOptions options,
BTCPayServerEnvironment btcPayServerEnvironment,
U2FService u2FService,
RateLimitService rateLimitService,
EventAggregator eventAggregator)
EventAggregator eventAggregator,
Fido2Service fido2Service)
{
_userManager = userManager;
_signInManager = signInManager;
@@ -57,7 +58,7 @@ namespace BTCPayServer.Controllers
_Options = options;
_btcPayServerEnvironment = btcPayServerEnvironment;
_u2FService = u2FService;
_rateLimitService = rateLimitService;
_fido2Service = fido2Service;
_eventAggregator = eventAggregator;
_logger = Logs.PayServer;
}
@@ -125,7 +126,9 @@ namespace BTCPayServer.Controllers
return View(model);
}
if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id))
var u2fDevices = await _u2FService.HasDevices(user.Id);
var fido2Devices = await _fido2Service.HasCredentials(user.Id);
if (!await _userManager.IsLockedOutAsync(user) && u2fDevices || fido2Devices)
{
if (await _userManager.CheckPasswordAsync(user, model.Password))
{
@@ -144,7 +147,8 @@ namespace BTCPayServer.Controllers
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = twoFModel,
LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user)
LoginWithU2FViewModel = u2fDevices? await BuildU2FViewModel(model.RememberMe, user) : null,
LoginWithFido2ViewModel = fido2Devices? await BuildFido2ViewModel(model.RememberMe, user): null,
});
}
else
@@ -210,6 +214,77 @@ namespace BTCPayServer.Controllers
return null;
}
private async Task<LoginWithFido2ViewModel> BuildFido2ViewModel(bool rememberMe, ApplicationUser user)
{
if (_btcPayServerEnvironment.IsSecure)
{
var r = await _fido2Service.RequestLogin(user.Id);
if (r is null)
{
return null;
}
return new LoginWithFido2ViewModel()
{
Data = r,
UserId = user.Id,
RememberMe = rememberMe
};
}
return null;
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWithFido2(LoginWithFido2ViewModel viewModel, string returnUrl = null)
{
if (!CanLoginOrRegister())
{
return RedirectToAction("Login");
}
ViewData["ReturnUrl"] = returnUrl;
var user = await _userManager.FindByIdAsync(viewModel.UserId);
if (user == null)
{
return NotFound();
}
var errorMessage = string.Empty;
try
{
if (await _fido2Service.CompleteLogin(viewModel.UserId, JObject.Parse(viewModel.Response).ToObject<AuthenticatorAssertionRawResponse>()))
{
await _signInManager.SignInAsync(user, viewModel.RememberMe, "FIDO2");
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
errorMessage = "Invalid login attempt.";
}
catch (U2fException e)
{
errorMessage = e.Message;
}
ModelState.AddModelError(string.Empty, errorMessage);
viewModel.Response = null;
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWithFido2ViewModel = viewModel,
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(viewModel.RememberMe, user) : null,
LoginWith2FaViewModel = !user.TwoFactorEnabled
? null
: new LoginWith2faViewModel()
{
RememberMe = viewModel.RememberMe
}
});
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
@@ -280,7 +355,8 @@ namespace BTCPayServer.Controllers
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null,
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
});
}
@@ -326,7 +402,8 @@ namespace BTCPayServer.Controllers
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = model,
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null,
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
});
}
}