Support LNURL Auth

This commit is contained in:
Kukks
2021-11-11 13:03:08 +01:00
committed by Andrew Camilleri
parent 1fb582c35d
commit 7243aec213
31 changed files with 697 additions and 46 deletions

View File

@@ -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)
{

View File

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

View File

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

View File

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

View File

@@ -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<ApplicationUser> _userManager;
private readonly LnurlAuthService _lnurlAuthService;
private readonly LinkGenerator _linkGenerator;
public LNURLAuthController(UserManager<ApplicationUser> 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<IActionResult> 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<IActionResult> 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<IActionResult> CreateCheck()
{
var userId = _userManager.GetUserId(User);
if (_lnurlAuthService.CreationStore.TryGetValue(userId, out _))
{
return Task.FromResult<IActionResult>(Ok());
}
return Task.FromResult<IActionResult>(NotFound());
}
[HttpGet("register-callback")]
[AllowAnonymous]
public async Task<IActionResult> 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<IActionResult> LoginCheck(string userId)
{
return _lnurlAuthService.LoginStore.ContainsKey(userId) ? Task.FromResult<IActionResult>(Ok()) : Task.FromResult<IActionResult>(NotFound());
}
[HttpGet("login-callback")]
[AllowAnonymous]
public async Task<IActionResult> 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");
}
}
}

View File

@@ -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<string, byte[]> CreationStore =
new ConcurrentDictionary<string, byte[]>();
public readonly ConcurrentDictionary<string, byte[]> LoginStore =
new ConcurrentDictionary<string, byte[]>();
public readonly ConcurrentDictionary<string, byte[]> FinalLoginStore =
new ConcurrentDictionary<string, byte[]>();
private readonly ApplicationDbContextFactory _contextFactory;
public LnurlAuthService(ApplicationDbContextFactory contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<byte[]> 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<bool> 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<byte[]> 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<bool> 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<bool> HasCredentials(string userId)
{
await using var context = _contextFactory.CreateContext();
return await context.Fido2Credentials.Where(fDevice => fDevice.ApplicationUserId == userId && fDevice.Type == Fido2Credential.CredentialType.LNURLAuth).AnyAsync();
}
}
}

View File

@@ -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))
{
@@ -166,6 +177,7 @@ namespace BTCPayServer.Controllers
{
LoginWith2FaViewModel = twoFModel,
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<LoginWithLNURLAuthViewModel> 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<IActionResult> 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,
});
}
}

View File

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

View File

@@ -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<bool> 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<AssertionOptions> 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)
{

View File

@@ -7,7 +7,7 @@ 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;

View File

@@ -111,7 +111,7 @@ namespace BTCPayServer.Hosting
});
services.AddScoped<Fido2Service>();
services.AddSingleton<UserLoginCodeService>();
services.AddSingleton<LnurlAuthService>();
var mvcBuilder = services.AddMvc(o =>
{
o.Filters.Add(new XFrameOptionsAttribute("DENY"));

View File

@@ -6,5 +6,6 @@ namespace BTCPayServer.Models.AccountViewModels
{
public LoginWithFido2ViewModel LoginWithFido2ViewModel { get; set; }
public LoginWith2faViewModel LoginWith2FaViewModel { get; set; }
public LoginWithLNURLAuthViewModel LoginWithLNURLAuthViewModel { get; set; }
}
}

View File

@@ -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> _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> btcPayServerOptions,
IWebHostEnvironment webHostEnvironment,
StoreRepository storeRepository,

View File

@@ -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)
{

View File

@@ -0,0 +1,64 @@
@using LNURL
@model Uri
@{
ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, "Register your Lightning node for LNURL Auth");
Dictionary<string, string> formats = new Dictionary<string, string>()
{
{ "Bech32", LNURL.EncodeUri(Model, "login", true).ToString().ToUpperInvariant() },
{ "URI", LNURL.EncodeUri(Model, "login", false).ToString().ToUpperInvariant() }
};
}
<div class="row">
<div class="col-lg-8">
<div id="info-message" class="align-items-center">
<div class="tab-content">
@for (int i = 0; i < formats.Count; i++)
{
var mode = formats.ElementAt(i);
<div class="tab-pane @(i == 0 ? "active" : "")" id="@mode.Key" role="tabpanel">
<div class="qr-container text-center" style="min-height: 256px;">
<vc:qr-code data="@mode.Value"></vc:qr-code>
</div>
<a href="@mode.Value" class="btn btn-primary w-100 mt-2" rel="noreferrer noopener">
Open in wallet
</a>
</div>
}
</div>
<ul class="nav justify-content-center bg-light text-dark my-2">
@for (int i = 0; i < formats.Count; i++)
{
var mode = formats.ElementAt(i);
<li class="nav-item">
<a class="nav-link @(i == 0 ? "active" : "")"
data-bs-toggle="tab" data-bs-target="#@mode.Key" role="tab"
href="#">
@mode.Key
</a>
</li>
}
</ul>
<span>Scan the QR code with your lightning wallet and link to your user account.</span>
</div>
</div>
</div>
<script>
function check(){
const request = new XMLHttpRequest();
request.onload = function() {
if (request.readyState === 4 && request.status === 200) {
setTimeout(check, 1000);
} else if (request.readyState === 4 ){
window.location.href = @Safe.Json(Url.Action("RedirectToList", new {successMessage = "The lightning node will now act as a security device for your account"}));
}
}
request.open("GET", window.location.pathname + "/check", true);
request.send(new FormData());
}
check();
</script>

View File

@@ -0,0 +1,2 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Views.Manage

View File

@@ -0,0 +1,6 @@
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewBag.MainTitle = "Manage your account";
ViewData["NavPartialName"] = "../UIManage/_Nav";
}

View File

@@ -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())
{

View File

@@ -26,7 +26,7 @@
Enabled
</span>
<span class="text-light ms-3 me-2">|</span>
<a lass="btn btn-link px-1 py-1 fw-semibold" asp-controller="Shopify" asp-action="EditShopifyIntegration" asp-route-storeId="@Context.GetRouteValue("storeId")">
<a lass="btn btn-link px-1 py-1 fw-semibold" asp-controller="UIShopify" asp-action="EditShopifyIntegration" asp-route-storeId="@Context.GetRouteValue("storeId")">
Modify
</a>
}
@@ -36,7 +36,7 @@
<span class="me-2 btcpay-status btcpay-status--disabled"></span>
Disabled
</span>
<a class="btn btn-primary btn-sm ms-4 px-3 py-1 fw-semibold" asp-controller="Shopify" asp-action="EditShopifyIntegration" asp-route-storeId="@Context.GetRouteValue("storeId")">
<a class="btn btn-primary btn-sm ms-4 px-3 py-1 fw-semibold" asp-controller="UIShopify" asp-action="EditShopifyIntegration" asp-route-storeId="@Context.GetRouteValue("storeId")">
Setup
</a>
}

View File

@@ -5,7 +5,7 @@
}
<li class="nav-item">
<a asp-area="" asp-controller="Shopify" asp-action="EditShopifyIntegration" asp-route-storeId="@store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage("shopify", nameof(StoreNavPages))" id="StoreNav-Shopify">
<a asp-area="" asp-controller="UIShopify" asp-action="EditShopifyIntegration" asp-route-storeId="@store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage("shopify", nameof(StoreNavPages))" id="StoreNav-Shopify">
<vc:icon symbol="shopify"/>
<span>Shopify</span>
</a>

View File

@@ -0,0 +1,72 @@
@model LoginWithLNURLAuthViewModel
@{
Dictionary<string, string> formats = new Dictionary<string, string>()
{
{ "Bech32", LNURL.LNURL.EncodeUri(Model.LNURLEndpoint, "login", true).ToString().ToUpperInvariant() },
{ "URI", LNURL.LNURL.EncodeUri(Model.LNURLEndpoint, "login", true).ToString().ToUpperInvariant() }
};
}
<section class="pt-5" id="lnurlauth-section">
<div>
<div class="row">
<div class="col-lg-12 section-heading">
<h2>LNURL Auth</h2>
<hr class="primary">
<div class="align-items-center">
<div class="tab-content">
@for (int i = 0; i < formats.Count; i++)
{
var mode = formats.ElementAt(i);
<div class="tab-pane @(i == 0 ? "active" : "")" id="@mode.Key" role="tabpanel">
<div class="qr-container text-center" style="min-height: 256px;">
<vc:qr-code data="@mode.Value"></vc:qr-code>
</div>
<a href="@mode.Value" class="btn btn-primary w-100 mt-2" rel="noreferrer noopener">
Open in wallet
</a>
</div>
}
</div>
<ul class="nav justify-content-center bg-light text-dark my-2">
@for (int i = 0; i < formats.Count; i++)
{
var mode = formats.ElementAt(i);
<li class="nav-item">
<a class="nav-link @(i == 0 ? "active" : "")"
data-bs-toggle="tab" data-bs-target="#@mode.Key" role="tab"
href="#">
@mode.Key
</a>
</li>
}
</ul>
<p>Scan the QR code with your lightning wallet and link to your user account.</p>
</div>
</div>
</div>
</div>
</section>
<form id="authform" asp-action="LoginWithLNURLAuth" method="post" asp-route-returnUrl="@ViewData["ReturnUrl"]">
<input type="hidden" asp-for="LNURLEndpoint"/>
<input type="hidden" asp-for="UserId"/>
</form>
<script>
function check(){
const request = new XMLHttpRequest();
request.onload = function() {
if (request.readyState === 4 && request.status === 200) {
setTimeout(check, 1000);
} else if (request.readyState === 4 ){
document.getElementById("authform").submit();
}
}
request.open("GET", @Safe.Json(Url.Action("LoginCheck", "LNURLAuth", new {userId = Model.UserId})), true);
request.send(new FormData());
}
check();
</script>

View File

@@ -3,20 +3,42 @@
ViewData["Title"] = "Two-factor/U2F authentication";
}
@if (Model.LoginWith2FaViewModel != null && Model.LoginWithFido2ViewModel != null)
@if (Model.LoginWith2FaViewModel != null && Model.LoginWithFido2ViewModel != null&& Model.LoginWithLNURLAuthViewModel != null)
{
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
}
else if (Model.LoginWith2FaViewModel == null && Model.LoginWithFido2ViewModel == null)
else if (Model.LoginWith2FaViewModel == null && Model.LoginWithFido2ViewModel == null && Model.LoginWithLNURLAuthViewModel == null)
{
<div class="row">
<div class="col-lg-12 section-heading">
<h2 class="bg-danger">Both 2FA and U2F/FIDO2 Authentication Methods are not available. Please go to the https endpoint.</h2>
<h2 class="bg-danger">2FA and U2F/FIDO2 and LNURL-Auth Authentication Methods are not available. Please go to the https endpoint.</h2>
<hr class="danger">
</div>
</div>
}
<div class="row justify-content-center">
@if (Model.LoginWith2FaViewModel != null)
{
<div class="col-sm-12 col-md-6">
<partial name="LoginWith2fa" model="@Model.LoginWith2FaViewModel"/>
</div>
}
@if (Model.LoginWithFido2ViewModel != null)
{
<div class="col-sm-12 col-md-6">
<partial name="LoginWithFido2" model="@Model.LoginWithFido2ViewModel"/>
</div>
}
@if (Model.LoginWithLNURLAuthViewModel != null)
{
<div class="col-sm-12 col-md-6">
<partial name="LoginWithLNURLAuth" model="@Model.LoginWithLNURLAuthViewModel"/>
</div>
}
</div>
}
<div class="row justify-content-center">
@if (Model.LoginWith2FaViewModel != null)
{

View File

@@ -1,6 +1,6 @@
@model Fido2NetLib.CredentialCreateOptions
@{
ViewData.SetActivePage(ManageNavPages.Fido2, "Register your security device");
ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, "Register your security device");
}
<form asp-action="CreateResponse" id="registerForm">

View File

@@ -11,8 +11,8 @@
<ul>
<li>
Microsoft Authenticator for
<a href="https://go.microsoft.com/fwlink/?Linkid=825072" rel="noreferrer noopener">Android</a> or
<a href="https://go.microsoft.com/fwlink/?Linkid=825073" rel="noreferrer noopener">iOS</a>
<a href="https://play.google.com/store/apps/details?id=com.azure.authenticator" rel="noreferrer noopener">Android</a> or
<a href="https://itunes.apple.com/us/app/microsoft-authenticator/id983156458" rel="noreferrer noopener">iOS</a>
</li>
<li>
Google Authenticator for

View File

@@ -2,6 +2,6 @@ namespace BTCPayServer.Views.Manage
{
public enum ManageNavPages
{
Index, ChangePassword, TwoFactorAuthentication, APIKeys, Notifications, Fido2, LoginCodes
Index, ChangePassword, TwoFactorAuthentication, APIKeys, Notifications, LoginCodes
}
}

View File

@@ -1,3 +1,4 @@
@using Fido2NetLib
@model TwoFactorAuthenticationViewModel
@{
ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, "Two-Factor Authentication");
@@ -102,20 +103,39 @@
{
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>
<div class="mb-0">
<h5 class="mb-0 w-100">@name</h5>
@switch (device.Type)
{
case Fido2Credential.CredentialType.FIDO2:
<span class="text-muted">Security device (FIDO2)</span>
break;
case Fido2Credential.CredentialType.LNURLAuth:
<span class="text-muted">Lightning node (LNURL Auth)</span>
break;
}
</div>
@if (device.Type == Fido2Credential.CredentialType.FIDO2)
{
<a asp-controller="UIFido2" 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>
}
else if (device.Type == Fido2Credential.CredentialType.LNURLAuth)
{
<a asp-controller="LNURLAuth" asp-action="Remove" asp-route-id="@device.Id" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Remove Lightning security" data-description="Your account will no longer be linked to the lightning node <strong>@name</strong> as an option for two-factor authentication." data-confirm="Remove" data-confirm-input="REMOVE">Remove</a>
}
</div>
}
</div>
}
<form asp-controller="UIFido2" asp-action="Create" method="get">
<form asp-action="CreateCredential">
<div class="input-group">
<input type="text" class="form-control" name="Name" placeholder="Security device name"/>
<button type="submit" class="btn btn-primary">
<select asp-items="@Html.GetEnumSelectList<Fido2Credential.CredentialType>()" class="form-select" name="type"></select>
<button id="btn-add" type="submit" class="btn btn-primary">
<span class="fa fa-plus"></span>
Add
<span class="d-none d-md-inline-block">Device</span>
</button>
</div>
</form>

View File

@@ -1,6 +1,6 @@
@using BTCPayServer.Views.Stores
@using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Services.Altcoins.Monero.UI.MoneroLikeStoreController.MoneroLikePaymentMethodViewModel
@model BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";

View File

@@ -1,6 +1,6 @@
@using BTCPayServer.Views.Stores
@using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Services.Altcoins.Monero.UI.MoneroLikeStoreController.MoneroLikePaymentMethodListViewModel
@model BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodListViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";

View File

@@ -65,7 +65,7 @@
In Shopify please paste following script at <a href="@shopifyUrl/admin/settings/checkout#PolarisTextField1" target="_blank" class="fw-bold" rel="noreferrer noopener"> Settings &gt; Checkout &gt; Order Processing &gt; Additional Scripts</a>
</p>
<kbd style="display: block; word-break: break-all;">
@($"<script src='{Url.Action("ShopifyJavascript", "Shopify", new { storeId = Context.GetRouteValue("storeId") }, Context.Request.Scheme)}'></script>")
@($"<script src='{Url.Action("ShopifyJavascript", "UIShopify", new { storeId = Context.GetRouteValue("storeId") }, Context.Request.Scheme)}'></script>")
</kbd>
</div>
<p class="alert alert-warning">

View File

@@ -189,7 +189,7 @@
<h4 class="mt-5 mb-3">Default Currency Pairs</h4>
<div class="form-group">
<label asp-for="DefaultCurrencyPairs" class="form-label">Query pairs via REST by querying <a asp-controller="UIRate" asp-action="GetRates2" asp-route-storeId="@Model.StoreId" target="_blank">this link</a> without the need to specify currencyPairs.</label>
<label asp-for="DefaultCurrencyPairs" class="form-label">Query pairs via REST by querying <a asp-controller="BitpayRate" asp-action="GetRates2" asp-route-storeId="@Model.StoreId" target="_blank">this link</a> without the need to specify currencyPairs.</label>
<input asp-for="DefaultCurrencyPairs" class="form-control" placeholder="BTC_USD, BTC_CAD" />
<span asp-validation-for="DefaultCurrencyPairs" class="text-danger"></span>
</div>