From 7243aec2137d94bcc3d48f2d8e3d4eef1dd5fc03 Mon Sep 17 00:00:00 2001 From: Kukks Date: Thu, 11 Nov 2021 13:03:08 +0100 Subject: [PATCH] Support LNURL Auth --- BTCPayServer.Data/Data/Fido2Credential.cs | 6 +- ...roller.cs => UITestExtensionController.cs} | 4 +- .../Index.cshtml | 0 BTCPayServer.Tests/SeleniumTests.cs | 50 ++++++ BTCPayServer.Tests/TestUtils.cs | 5 + .../Controllers/LNURLAuthController.cs | 151 ++++++++++++++++++ BTCPayServer/Controllers/LnurlAuthService.cs | 147 +++++++++++++++++ .../Controllers/UIAccountController.cs | 97 ++++++++++- .../Controllers/UIManageController.2FA.cs | 14 ++ BTCPayServer/Fido2/Fido2Service.cs | 8 +- BTCPayServer/Fido2/FidoExtensions.cs | 6 +- BTCPayServer/Hosting/Startup.cs | 2 +- .../SecondaryLoginViewModel.cs | 1 + ...fyController.cs => UIShopifyController.cs} | 4 +- .../Monero/UI/MoneroLikeStoreController.cs | 4 +- BTCPayServer/Views/LNURLAuth/Create.cshtml | 64 ++++++++ .../Views/LNURLAuth/_ViewImports.cshtml | 2 + .../Views/LNURLAuth/_ViewStart.cshtml | 6 + .../Monero/StoreNavMoneroExtension.cshtml | 2 +- .../Shopify/StoreIntegrationsList.cshtml | 4 +- .../Shopify/StoreIntegrationsNav.cshtml | 2 +- .../Views/UIAccount/LoginWithLNURLAuth.cshtml | 72 +++++++++ .../Views/UIAccount/SecondaryLogin.cshtml | 44 +++-- BTCPayServer/Views/UIFido2/Create.cshtml | 2 +- .../Views/UIManage/EnableAuthenticator.cshtml | 4 +- BTCPayServer/Views/UIManage/ManageNavPages.cs | 2 +- .../UIManage/TwoFactorAuthentication.cshtml | 32 +++- .../GetStoreMoneroLikePaymentMethod.cshtml | 2 +- .../GetStoreMoneroLikePaymentMethods.cshtml | 2 +- .../UIShopify/EditShopifyIntegration.cshtml | 2 +- BTCPayServer/Views/UIStores/Rates.cshtml | 2 +- 31 files changed, 697 insertions(+), 46 deletions(-) rename BTCPayServer.Plugins.Test/Controllers/{TestExtensionController.cs => UITestExtensionController.cs} (84%) rename BTCPayServer.Plugins.Test/Views/{TestExtension => UITestExtension}/Index.cshtml (100%) create mode 100644 BTCPayServer/Controllers/LNURLAuthController.cs create mode 100644 BTCPayServer/Controllers/LnurlAuthService.cs rename BTCPayServer/Plugins/Shopify/{ShopifyController.cs => UIShopifyController.cs} (98%) create mode 100644 BTCPayServer/Views/LNURLAuth/Create.cshtml create mode 100644 BTCPayServer/Views/LNURLAuth/_ViewImports.cshtml create mode 100644 BTCPayServer/Views/LNURLAuth/_ViewStart.cshtml create mode 100644 BTCPayServer/Views/UIAccount/LoginWithLNURLAuth.cshtml diff --git a/BTCPayServer.Data/Data/Fido2Credential.cs b/BTCPayServer.Data/Data/Fido2Credential.cs index 6f3b0c415..101b6f82e 100644 --- a/BTCPayServer.Data/Data/Fido2Credential.cs +++ b/BTCPayServer.Data/Data/Fido2Credential.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; @@ -16,7 +17,10 @@ namespace BTCPayServer.Data public CredentialType Type { get; set; } public enum CredentialType { - FIDO2 + [Display(Name = "Security device (FIDO2)")] + FIDO2, + [Display(Name = "Lightning node (LNURL Auth)")] + LNURLAuth } public static void OnModelCreating(ModelBuilder builder) { diff --git a/BTCPayServer.Plugins.Test/Controllers/TestExtensionController.cs b/BTCPayServer.Plugins.Test/Controllers/UITestExtensionController.cs similarity index 84% rename from BTCPayServer.Plugins.Test/Controllers/TestExtensionController.cs rename to BTCPayServer.Plugins.Test/Controllers/UITestExtensionController.cs index 5f36e4c79..4c3fcb349 100644 --- a/BTCPayServer.Plugins.Test/Controllers/TestExtensionController.cs +++ b/BTCPayServer.Plugins.Test/Controllers/UITestExtensionController.cs @@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Plugins.Test { [Route("extensions/test")] - public class TestExtensionController : Controller + public class UITestExtensionController : Controller { private readonly TestPluginService _testPluginService; - public TestExtensionController(TestPluginService testPluginService) + public UITestExtensionController(TestPluginService testPluginService) { _testPluginService = testPluginService; } diff --git a/BTCPayServer.Plugins.Test/Views/TestExtension/Index.cshtml b/BTCPayServer.Plugins.Test/Views/UITestExtension/Index.cshtml similarity index 100% rename from BTCPayServer.Plugins.Test/Views/TestExtension/Index.cshtml rename to BTCPayServer.Plugins.Test/Views/UITestExtension/Index.cshtml diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 8152717a2..753162b35 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -1644,6 +1644,56 @@ retry: } } + + [Fact] + [Trait("Selenium", "Selenium")] + public async Task CanUseLNURLAuth() + { + using var s = CreateSeleniumTester(); + await s.StartAsync(); + var user = s.RegisterNewUser(true); + s.GoToProfile(ManageNavPages.TwoFactorAuthentication); + s.Driver.FindElement(By.Name("Name")).SendKeys("ln wallet"); + s.Driver.FindElement(By.Name("type")) + .FindElement(By.CssSelector($"option[value='{(int)Fido2Credential.CredentialType.LNURLAuth}']")).Click(); + s.Driver.FindElement(By.Id("btn-add")).Click(); + var links = s.Driver.FindElements(By.CssSelector(".tab-content a")).Select(element => element.GetAttribute("href")); + Assert.Equal(2,links.Count()); + Uri prevEndpoint = null; + foreach (string link in links) + { + var endpoint = LNURL.LNURL.Parse(link, out var tag); + Assert.Equal("login",tag); + if(endpoint.Scheme != "https") + prevEndpoint = endpoint; + } + + var linkingKey = new Key(); + var request = Assert.IsType(await LNURL.LNURL.FetchInformation(prevEndpoint, null)); + _ = await request.SendChallenge(linkingKey, new HttpClient()); + TestUtils.Eventually(() => s.FindAlertMessage()); + + s.Logout(); + s.Login(user, "123456"); + var section = s.Driver.FindElement(By.Id("lnurlauth-section")); + links = section.FindElements(By.CssSelector(".tab-content a")).Select(element => element.GetAttribute("href")); + Assert.Equal(2,links.Count()); + prevEndpoint = null; + foreach (string link in links) + { + var endpoint = LNURL.LNURL.Parse(link, out var tag); + Assert.Equal("login",tag); + if(endpoint.Scheme != "https") + prevEndpoint = endpoint; + } + request = Assert.IsType(await LNURL.LNURL.FetchInformation(prevEndpoint, null)); + _ = await request.SendChallenge(linkingKey, new HttpClient()); + TestUtils.Eventually(() => + { + Assert.Equal(s.Driver.Url, s.ServerUri.ToString()); + }); + } + private static void CanBrowseContent(SeleniumTester s) { s.Driver.FindElement(By.ClassName("delivery-content")).Click(); diff --git a/BTCPayServer.Tests/TestUtils.cs b/BTCPayServer.Tests/TestUtils.cs index e32e690e1..1a233a838 100644 --- a/BTCPayServer.Tests/TestUtils.cs +++ b/BTCPayServer.Tests/TestUtils.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using OpenQA.Selenium; using Xunit; using Xunit.Sdk; @@ -88,6 +89,10 @@ namespace BTCPayServer.Tests act(); break; } + catch (WebDriverException) when (!cts.Token.IsCancellationRequested) + { + cts.Token.WaitHandle.WaitOne(500); + } catch (XunitException) when (!cts.Token.IsCancellationRequested) { cts.Token.WaitHandle.WaitOne(500); diff --git a/BTCPayServer/Controllers/LNURLAuthController.cs b/BTCPayServer/Controllers/LNURLAuthController.cs new file mode 100644 index 000000000..022ed13df --- /dev/null +++ b/BTCPayServer/Controllers/LNURLAuthController.cs @@ -0,0 +1,151 @@ +using System; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Data; +using BTCPayServer.Models; +using LNURL; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using NBitcoin; +using NBitcoin.Crypto; +using NBitcoin.DataEncoders; + +namespace BTCPayServer +{ + [Route("lnurlauth")] + [Authorize] + public class LNURLAuthController : Controller + { + private readonly UserManager _userManager; + private readonly LnurlAuthService _lnurlAuthService; + private readonly LinkGenerator _linkGenerator; + + public LNURLAuthController(UserManager userManager, LnurlAuthService lnurlAuthService, + LinkGenerator linkGenerator) + { + _userManager = userManager; + _lnurlAuthService = lnurlAuthService; + _linkGenerator = linkGenerator; + } + + [HttpGet("{id}/delete")] + public IActionResult Remove(string id) + { + return View("Confirm", + new ConfirmModel("Remove LNURL Auth link", + "Your account will no longer have this Lightning wallet as an option for two-factor authentication.", + "Remove")); + } + + [HttpPost("{id}/delete")] + public async Task RemoveP(string id) + { + await _lnurlAuthService.Remove(id, _userManager.GetUserId(User)); + + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = StatusMessageModel.StatusSeverity.Success, Html = "LNURL Auth was removed successfully." + }); + + return RedirectToList(); + } + + [HttpGet("register")] + public async Task Create(string name) + { + var userId = _userManager.GetUserId(User); + var options = await _lnurlAuthService.RequestCreation(userId); + if (options is null) + { + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = StatusMessageModel.StatusSeverity.Error, + Html = "The lightning node could not be registered." + }); + + return RedirectToList(); + } + + return View(new Uri(_linkGenerator.GetUriByAction( + action: nameof(CreateResponse), + controller: "LNURLAuth", + values: new + { + userId, + name, + tag = "login", + action = "link", + k1 = Encoders.Hex.EncodeData(options) + }, Request.Scheme, Request.Host, Request.PathBase) ?? string.Empty)); + } + + [HttpGet("register/check")] + public Task CreateCheck() + { + var userId = _userManager.GetUserId(User); + if (_lnurlAuthService.CreationStore.TryGetValue(userId, out _)) + { + return Task.FromResult(Ok()); + } + + return Task.FromResult(NotFound()); + } + + [HttpGet("register-callback")] + [AllowAnonymous] + public async Task CreateResponse(string userId, string sig, string key, string name) + { + if (await _lnurlAuthService.CompleteCreation(name, userId, + ECDSASignature.FromDER(Encoders.Hex.DecodeData(sig)), new PubKey(key))) + { + return Ok(new LNUrlStatusResponse() { Status = "OK" }); + } + + return BadRequest(new LNUrlStatusResponse() + { + Reason = "The challenge could not be verified", Status = "ERROR" + }); + } + + + [HttpGet("login-check")] + [AllowAnonymous] + public Task LoginCheck(string userId) + { + return _lnurlAuthService.LoginStore.ContainsKey(userId) ? Task.FromResult(Ok()) : Task.FromResult(NotFound()); + } + + [HttpGet("login-callback")] + [AllowAnonymous] + public async Task LoginResponse(string userId, string sig, string key) + { + if (await _lnurlAuthService.CompleteLogin(userId, + ECDSASignature.FromDER(Encoders.Hex.DecodeData(sig)), new PubKey(key))) + { + return Ok(new LNUrlStatusResponse() { Status = "OK" }); + } + + return BadRequest(new LNUrlStatusResponse() + { + Reason = "The challenge could not be verified", Status = "ERROR" + }); + } + + public ActionResult RedirectToList(string successMessage = null) + { + if (successMessage != null) + { + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = StatusMessageModel.StatusSeverity.Success, + Html = successMessage + }); + } + + return RedirectToAction("TwoFactorAuthentication", "UIManage"); + } + } +} diff --git a/BTCPayServer/Controllers/LnurlAuthService.cs b/BTCPayServer/Controllers/LnurlAuthService.cs new file mode 100644 index 000000000..4b304d6dd --- /dev/null +++ b/BTCPayServer/Controllers/LnurlAuthService.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Fido2; +using Microsoft.EntityFrameworkCore; +using NBitcoin; +using NBitcoin.Crypto; + +namespace BTCPayServer +{ + public class LoginWithLNURLAuthViewModel + { + public string UserId { get; set; } + public Uri LNURLEndpoint { get; set; } + public bool RememberMe { get; set; } + } + + public class LnurlAuthService + { + public readonly ConcurrentDictionary CreationStore = + new ConcurrentDictionary(); + public readonly ConcurrentDictionary LoginStore = + new ConcurrentDictionary(); + public readonly ConcurrentDictionary FinalLoginStore = + new ConcurrentDictionary(); + private readonly ApplicationDbContextFactory _contextFactory; + + public LnurlAuthService(ApplicationDbContextFactory contextFactory) + { + _contextFactory = contextFactory; + } + + public async Task RequestCreation(string userId) + { + await using var dbContext = _contextFactory.CreateContext(); + var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials) + .FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId); + if (user == null) + { + return null; + } + var k1 = RandomUtils.GetBytes(32); + CreationStore.AddOrReplace(userId, k1); + return k1; + } + + public async Task CompleteCreation(string name, string userId, ECDSASignature sig, PubKey pubKey) + { + try + { + await using var dbContext = _contextFactory.CreateContext(); + var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials) + .FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId); + var pubkeyBytes = pubKey.ToBytes(); + if (!CreationStore.TryGetValue(userId.ToLowerInvariant(), out var k1) || user == null || await dbContext.Fido2Credentials.AnyAsync(credential => credential.Type == Fido2Credential.CredentialType.LNURLAuth && credential.Blob == pubkeyBytes)) + { + return false; + } + + if (!global::LNURL.LNAuthRequest.VerifyChallenge(sig, pubKey, k1)) + { + return false; + } + + var newCredential = new Fido2Credential() {Name = name, ApplicationUserId = userId, Type = Fido2Credential.CredentialType.LNURLAuth, Blob = pubkeyBytes}; + + await dbContext.Fido2Credentials.AddAsync(newCredential); + await dbContext.SaveChangesAsync(); + CreationStore.Remove(userId, out _); + return true; + } + catch (Exception) + { + return false; + } + } + + public async Task Remove(string id, string userId) + { + await using var context = _contextFactory.CreateContext(); + var device = await context.Fido2Credentials.FindAsync( id); + if (device == null || !device.ApplicationUserId.Equals(userId, StringComparison.InvariantCulture)) + { + return; + } + + context.Fido2Credentials.Remove(device); + await context.SaveChangesAsync(); + } + + + public async Task RequestLogin(string userId) + { + await using var dbContext = _contextFactory.CreateContext(); + var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials) + .FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId); + if (!(user?.Fido2Credentials?.Any(credential => credential.Type == Fido2Credential.CredentialType.LNURLAuth) is true)) + { + return null; + } + + var k1 = RandomUtils.GetBytes(32); + + FinalLoginStore.TryRemove(userId, out _); + LoginStore.AddOrReplace(userId, k1); + return k1; + } + + public async Task CompleteLogin(string userId, ECDSASignature sig, PubKey pubKey){ + await using var dbContext = _contextFactory.CreateContext(); + userId = userId.ToLowerInvariant(); + var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials) + .FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId); + if (user == null || !LoginStore.TryGetValue(userId, out var k1)) + { + return false; + } + + var pubKeyBytes = pubKey.ToBytes(); + var credential = user.Fido2Credentials + .Where(fido2Credential => fido2Credential.Type is Fido2Credential.CredentialType.LNURLAuth) + .FirstOrDefault(fido2Credential => fido2Credential.Blob.SequenceEqual(pubKeyBytes)); + if (credential is null) + { + return false; + } + if (!global::LNURL.LNAuthRequest.VerifyChallenge(sig, pubKey, k1)) + { + return false; + } + LoginStore.Remove(userId, out _); + + FinalLoginStore.AddOrReplace(userId, k1); + // 7. return OK to client + return true; + } + + public async Task HasCredentials(string userId) + { + await using var context = _contextFactory.CreateContext(); + return await context.Fido2Credentials.Where(fDevice => fDevice.ApplicationUserId == userId && fDevice.Type == Fido2Credential.CredentialType.LNURLAuth).AnyAsync(); + } + } +} diff --git a/BTCPayServer/Controllers/UIAccountController.cs b/BTCPayServer/Controllers/UIAccountController.cs index 78ac68c08..957d129db 100644 --- a/BTCPayServer/Controllers/UIAccountController.cs +++ b/BTCPayServer/Controllers/UIAccountController.cs @@ -1,6 +1,8 @@ using System; using System.Globalization; using System.Security.Claims; +using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Extensions; @@ -17,7 +19,9 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; +using NBitcoin.DataEncoders; using Newtonsoft.Json.Linq; using NicolasDorier.RateLimits; @@ -33,6 +37,8 @@ namespace BTCPayServer.Controllers readonly Configuration.BTCPayServerOptions _Options; private readonly BTCPayServerEnvironment _btcPayServerEnvironment; private readonly Fido2Service _fido2Service; + private readonly LnurlAuthService _lnurlAuthService; + private readonly LinkGenerator _linkGenerator; private readonly UserLoginCodeService _userLoginCodeService; private readonly EventAggregator _eventAggregator; readonly ILogger _logger; @@ -49,6 +55,8 @@ namespace BTCPayServer.Controllers EventAggregator eventAggregator, Fido2Service fido2Service, UserLoginCodeService userLoginCodeService, + LnurlAuthService lnurlAuthService, + LinkGenerator linkGenerator, Logs logs) { _userManager = userManager; @@ -58,6 +66,8 @@ namespace BTCPayServer.Controllers _Options = options; _btcPayServerEnvironment = btcPayServerEnvironment; _fido2Service = fido2Service; + _lnurlAuthService = lnurlAuthService; + _linkGenerator = linkGenerator; _userLoginCodeService = userLoginCodeService; _eventAggregator = eventAggregator; _logger = logs.PayServer; @@ -146,7 +156,8 @@ namespace BTCPayServer.Controllers } var fido2Devices = await _fido2Service.HasCredentials(user.Id); - if (!await _userManager.IsLockedOutAsync(user) && fido2Devices) + var lnurlAuthCredentials = await _lnurlAuthService.HasCredentials(user.Id); + if (!await _userManager.IsLockedOutAsync(user) && (fido2Devices || lnurlAuthCredentials)) { if (await _userManager.CheckPasswordAsync(user, model.Password)) { @@ -165,7 +176,8 @@ namespace BTCPayServer.Controllers return View("SecondaryLogin", new SecondaryLoginViewModel() { LoginWith2FaViewModel = twoFModel, - LoginWithFido2ViewModel = fido2Devices ? await BuildFido2ViewModel(model.RememberMe, user) : null, + LoginWithFido2ViewModel = fido2Devices? await BuildFido2ViewModel(model.RememberMe, user): null, + LoginWithLNURLAuthViewModel = lnurlAuthCredentials? await BuildLNURLAuthViewModel(model.RememberMe, user): null, }); } else @@ -229,6 +241,84 @@ namespace BTCPayServer.Controllers return null; } + + private async Task BuildLNURLAuthViewModel(bool rememberMe, ApplicationUser user) + { + if (_btcPayServerEnvironment.IsSecure) + { + var r = await _lnurlAuthService.RequestLogin(user.Id); + if (r is null) + { + return null; + } + return new LoginWithLNURLAuthViewModel() + { + + RememberMe = rememberMe, + UserId = user.Id, + LNURLEndpoint = new Uri(_linkGenerator.GetUriByAction( + action: nameof(LNURLAuthController.LoginResponse), + controller: "LNURLAuth", + values: new { userId = user.Id, action="login", tag="login", k1= Encoders.Hex.EncodeData(r) }, Request.Scheme, Request.Host, Request.PathBase)) + }; + } + return null; + } + + [HttpPost("/login/lnurlauth")] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task LoginWithLNURLAuth(LoginWithLNURLAuthViewModel 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 + { + var k1 = Encoders.Hex.DecodeData(viewModel.LNURLEndpoint.ParseQueryString().Get("k1")); + if (_lnurlAuthService.FinalLoginStore.TryRemove(viewModel.UserId, out var storedk1) && + storedk1.SequenceEqual(k1)) + { + _lnurlAuthService.FinalLoginStore.TryRemove(viewModel.UserId, out _); + await _signInManager.SignInAsync(user, viewModel.RememberMe, "FIDO2"); + _logger.LogInformation("User logged in."); + return RedirectToLocal(returnUrl); + } + + errorMessage = "Invalid login attempt."; + } + catch (Exception e) + { + errorMessage = e.Message; + } + + ModelState.AddModelError(string.Empty, errorMessage); + return View("SecondaryLogin", new SecondaryLoginViewModel() + { + + LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(viewModel.RememberMe, user) : null, + LoginWithLNURLAuthViewModel = viewModel, + LoginWith2FaViewModel = !user.TwoFactorEnabled + ? null + : new LoginWith2faViewModel() + { + RememberMe = viewModel.RememberMe + } + }); + } + + [HttpPost("/login/fido2")] [AllowAnonymous] [ValidateAntiForgeryToken] @@ -269,6 +359,7 @@ namespace BTCPayServer.Controllers return View("SecondaryLogin", new SecondaryLoginViewModel() { LoginWithFido2ViewModel = viewModel, + LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(viewModel.RememberMe, user) : null, LoginWith2FaViewModel = !user.TwoFactorEnabled ? null : new LoginWith2faViewModel() @@ -300,6 +391,7 @@ namespace BTCPayServer.Controllers { LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe }, LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null, + LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(rememberMe, user) : null, }); } @@ -346,6 +438,7 @@ namespace BTCPayServer.Controllers { LoginWith2FaViewModel = model, LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null, + LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(rememberMe, user) : null, }); } } diff --git a/BTCPayServer/Controllers/UIManageController.2FA.cs b/BTCPayServer/Controllers/UIManageController.2FA.cs index 4500573ac..46a2656b9 100644 --- a/BTCPayServer/Controllers/UIManageController.2FA.cs +++ b/BTCPayServer/Controllers/UIManageController.2FA.cs @@ -180,5 +180,19 @@ namespace BTCPayServer.Controllers model.SharedKey = FormatKey(unformattedKey); model.AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); } + + [HttpPost] + public IActionResult CreateCredential(string name, Fido2Credential.CredentialType type) + { + switch (type) + { + case Fido2Credential.CredentialType.FIDO2: + return RedirectToAction("Create", "UIFido2", new { name }); + case Fido2Credential.CredentialType.LNURLAuth: + return RedirectToAction("Create", "LNURLAuth", new { name }); + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } } } diff --git a/BTCPayServer/Fido2/Fido2Service.cs b/BTCPayServer/Fido2/Fido2Service.cs index 4c48d2745..e6e9cd6ae 100644 --- a/BTCPayServer/Fido2/Fido2Service.cs +++ b/BTCPayServer/Fido2/Fido2Service.cs @@ -45,7 +45,7 @@ namespace BTCPayServer.Fido2 var existingKeys = user.Fido2Credentials .Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2) - .Select(c => c.GetBlob().Descriptor).ToList(); + .Select(c => c.GetFido2Blob().Descriptor).ToList(); // 3. Create options var authenticatorSelection = new AuthenticatorSelection @@ -144,7 +144,7 @@ namespace BTCPayServer.Fido2 public async Task HasCredentials(string userId) { await using var context = _contextFactory.CreateContext(); - return await context.Fido2Credentials.Where(fDevice => fDevice.ApplicationUserId == userId).AnyAsync(); + return await context.Fido2Credentials.Where(fDevice => fDevice.ApplicationUserId == userId && fDevice.Type == Fido2Credential.CredentialType.FIDO2).AnyAsync(); } public async Task RequestLogin(string userId) @@ -158,7 +158,7 @@ namespace BTCPayServer.Fido2 } var existingCredentials = user.Fido2Credentials .Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2) - .Select(c => c.GetBlob().Descriptor) + .Select(c => c.GetFido2Blob().Descriptor) .ToList(); var exts = new AuthenticationExtensionsClientInputs() { @@ -197,7 +197,7 @@ namespace BTCPayServer.Fido2 var credential = user.Fido2Credentials .Where(fido2Credential => fido2Credential.Type is Fido2Credential.CredentialType.FIDO2) - .Select(fido2Credential => (fido2Credential, fido2Credential.GetBlob())) + .Select(fido2Credential => (fido2Credential, fido2Credential.GetFido2Blob())) .FirstOrDefault(fido2Credential => fido2Credential.Item2.Descriptor.Id.SequenceEqual(response.Id)); if (credential.Item2 is null) { diff --git a/BTCPayServer/Fido2/FidoExtensions.cs b/BTCPayServer/Fido2/FidoExtensions.cs index 2d4e26276..7d4780c68 100644 --- a/BTCPayServer/Fido2/FidoExtensions.cs +++ b/BTCPayServer/Fido2/FidoExtensions.cs @@ -6,8 +6,8 @@ using Newtonsoft.Json.Linq; namespace BTCPayServer.Fido2 { public static class Fido2Extensions - { - public static Fido2CredentialBlob GetBlob(this Fido2Credential credential) + { + public static Fido2CredentialBlob GetFido2Blob(this Fido2Credential credential) { var result = credential.Blob == null ? new Fido2CredentialBlob() @@ -16,7 +16,7 @@ namespace BTCPayServer.Fido2 } public static bool SetBlob(this Fido2Credential credential, Fido2CredentialBlob descriptor) { - var original = new Serializer(null).ToString(credential.GetBlob()); + var original = new Serializer(null).ToString(credential.GetFido2Blob()); var newBlob = new Serializer(null).ToString(descriptor); if (original == newBlob) return false; diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 44219eb2d..40fadf76f 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -111,7 +111,7 @@ namespace BTCPayServer.Hosting }); services.AddScoped(); services.AddSingleton(); - + services.AddSingleton(); var mvcBuilder = services.AddMvc(o => { o.Filters.Add(new XFrameOptionsAttribute("DENY")); diff --git a/BTCPayServer/Models/AccountViewModels/SecondaryLoginViewModel.cs b/BTCPayServer/Models/AccountViewModels/SecondaryLoginViewModel.cs index 92537d716..283d452f5 100644 --- a/BTCPayServer/Models/AccountViewModels/SecondaryLoginViewModel.cs +++ b/BTCPayServer/Models/AccountViewModels/SecondaryLoginViewModel.cs @@ -6,5 +6,6 @@ namespace BTCPayServer.Models.AccountViewModels { public LoginWithFido2ViewModel LoginWithFido2ViewModel { get; set; } public LoginWith2faViewModel LoginWith2FaViewModel { get; set; } + public LoginWithLNURLAuthViewModel LoginWithLNURLAuthViewModel { get; set; } } } diff --git a/BTCPayServer/Plugins/Shopify/ShopifyController.cs b/BTCPayServer/Plugins/Shopify/UIShopifyController.cs similarity index 98% rename from BTCPayServer/Plugins/Shopify/ShopifyController.cs rename to BTCPayServer/Plugins/Shopify/UIShopifyController.cs index af5d3e42f..20a2e2996 100644 --- a/BTCPayServer/Plugins/Shopify/ShopifyController.cs +++ b/BTCPayServer/Plugins/Shopify/UIShopifyController.cs @@ -31,7 +31,7 @@ namespace BTCPayServer.Plugins.Shopify [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public class ShopifyController : Controller + public class UIShopifyController : Controller { private readonly BTCPayServerEnvironment _btcPayServerEnvironment; private readonly IOptions _btcPayServerOptions; @@ -42,7 +42,7 @@ namespace BTCPayServer.Plugins.Shopify private readonly IJsonHelper _jsonHelper; private readonly IHttpClientFactory _clientFactory; - public ShopifyController(BTCPayServerEnvironment btcPayServerEnvironment, + public UIShopifyController(BTCPayServerEnvironment btcPayServerEnvironment, IOptions btcPayServerOptions, IWebHostEnvironment webHostEnvironment, StoreRepository storeRepository, diff --git a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs index 16179c8d9..e7f611182 100644 --- a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs +++ b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs @@ -33,14 +33,14 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public class MoneroLikeStoreController : Controller + public class UIMoneroLikeStoreController : Controller { private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; private readonly StoreRepository _StoreRepository; private readonly MoneroRPCProvider _MoneroRpcProvider; private readonly BTCPayNetworkProvider _BtcPayNetworkProvider; - public MoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration, + public UIMoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration, StoreRepository storeRepository, MoneroRPCProvider moneroRpcProvider, BTCPayNetworkProvider btcPayNetworkProvider) { diff --git a/BTCPayServer/Views/LNURLAuth/Create.cshtml b/BTCPayServer/Views/LNURLAuth/Create.cshtml new file mode 100644 index 000000000..389e61104 --- /dev/null +++ b/BTCPayServer/Views/LNURLAuth/Create.cshtml @@ -0,0 +1,64 @@ +@using LNURL +@model Uri +@{ + ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, "Register your Lightning node for LNURL Auth"); + Dictionary formats = new Dictionary() + { + { "Bech32", LNURL.EncodeUri(Model, "login", true).ToString().ToUpperInvariant() }, + { "URI", LNURL.EncodeUri(Model, "login", false).ToString().ToUpperInvariant() } + }; + + +} + +
+
+
+
+ @for (int i = 0; i < formats.Count; i++) + { + var mode = formats.ElementAt(i); + + } +
+ + + Scan the QR code with your lightning wallet and link to your user account. +
+
+
+ diff --git a/BTCPayServer/Views/LNURLAuth/_ViewImports.cshtml b/BTCPayServer/Views/LNURLAuth/_ViewImports.cshtml new file mode 100644 index 000000000..d59f27dd1 --- /dev/null +++ b/BTCPayServer/Views/LNURLAuth/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Views.Manage diff --git a/BTCPayServer/Views/LNURLAuth/_ViewStart.cshtml b/BTCPayServer/Views/LNURLAuth/_ViewStart.cshtml new file mode 100644 index 000000000..ad5b74731 --- /dev/null +++ b/BTCPayServer/Views/LNURLAuth/_ViewStart.cshtml @@ -0,0 +1,6 @@ +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewBag.MainTitle = "Manage your account"; + + ViewData["NavPartialName"] = "../UIManage/_Nav"; +} diff --git a/BTCPayServer/Views/Shared/Monero/StoreNavMoneroExtension.cshtml b/BTCPayServer/Views/Shared/Monero/StoreNavMoneroExtension.cshtml index b775fdc6a..97f68afd4 100644 --- a/BTCPayServer/Views/Shared/Monero/StoreNavMoneroExtension.cshtml +++ b/BTCPayServer/Views/Shared/Monero/StoreNavMoneroExtension.cshtml @@ -4,7 +4,7 @@ @inject MoneroLikeConfiguration MoneroLikeConfiguration; @{ var controller = ViewContext.RouteData.Values["Controller"].ToString(); - var isMonero = controller.Equals(nameof(MoneroLikeStoreController), StringComparison.InvariantCultureIgnoreCase); + var isMonero = controller.Equals(nameof(UIMoneroLikeStoreController), StringComparison.InvariantCultureIgnoreCase); } @if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any()) { diff --git a/BTCPayServer/Views/Shared/Shopify/StoreIntegrationsList.cshtml b/BTCPayServer/Views/Shared/Shopify/StoreIntegrationsList.cshtml index 66c11e79e..94131f01f 100644 --- a/BTCPayServer/Views/Shared/Shopify/StoreIntegrationsList.cshtml +++ b/BTCPayServer/Views/Shared/Shopify/StoreIntegrationsList.cshtml @@ -26,7 +26,7 @@ Enabled | - + Modify } @@ -36,7 +36,7 @@ Disabled - + Setup } diff --git a/BTCPayServer/Views/Shared/Shopify/StoreIntegrationsNav.cshtml b/BTCPayServer/Views/Shared/Shopify/StoreIntegrationsNav.cshtml index e98993657..dda93ec85 100644 --- a/BTCPayServer/Views/Shared/Shopify/StoreIntegrationsNav.cshtml +++ b/BTCPayServer/Views/Shared/Shopify/StoreIntegrationsNav.cshtml @@ -5,7 +5,7 @@ }