mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 22:44:29 +01:00
Support LNURL Auth
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -16,7 +17,10 @@ namespace BTCPayServer.Data
|
|||||||
public CredentialType Type { get; set; }
|
public CredentialType Type { get; set; }
|
||||||
public enum CredentialType
|
public enum CredentialType
|
||||||
{
|
{
|
||||||
FIDO2
|
[Display(Name = "Security device (FIDO2)")]
|
||||||
|
FIDO2,
|
||||||
|
[Display(Name = "Lightning node (LNURL Auth)")]
|
||||||
|
LNURLAuth
|
||||||
}
|
}
|
||||||
public static void OnModelCreating(ModelBuilder builder)
|
public static void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
namespace BTCPayServer.Plugins.Test
|
namespace BTCPayServer.Plugins.Test
|
||||||
{
|
{
|
||||||
[Route("extensions/test")]
|
[Route("extensions/test")]
|
||||||
public class TestExtensionController : Controller
|
public class UITestExtensionController : Controller
|
||||||
{
|
{
|
||||||
private readonly TestPluginService _testPluginService;
|
private readonly TestPluginService _testPluginService;
|
||||||
|
|
||||||
public TestExtensionController(TestPluginService testPluginService)
|
public UITestExtensionController(TestPluginService testPluginService)
|
||||||
{
|
{
|
||||||
_testPluginService = testPluginService;
|
_testPluginService = testPluginService;
|
||||||
}
|
}
|
||||||
@@ -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)
|
private static void CanBrowseContent(SeleniumTester s)
|
||||||
{
|
{
|
||||||
s.Driver.FindElement(By.ClassName("delivery-content")).Click();
|
s.Driver.FindElement(By.ClassName("delivery-content")).Click();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using OpenQA.Selenium;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Sdk;
|
using Xunit.Sdk;
|
||||||
|
|
||||||
@@ -88,6 +89,10 @@ namespace BTCPayServer.Tests
|
|||||||
act();
|
act();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
catch (WebDriverException) when (!cts.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
cts.Token.WaitHandle.WaitOne(500);
|
||||||
|
}
|
||||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
cts.Token.WaitHandle.WaitOne(500);
|
cts.Token.WaitHandle.WaitOne(500);
|
||||||
|
|||||||
151
BTCPayServer/Controllers/LNURLAuthController.cs
Normal file
151
BTCPayServer/Controllers/LNURLAuthController.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
147
BTCPayServer/Controllers/LnurlAuthService.cs
Normal file
147
BTCPayServer/Controllers/LnurlAuthService.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
@@ -17,7 +19,9 @@ using Microsoft.AspNetCore.Authentication;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NBitcoin.DataEncoders;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using NicolasDorier.RateLimits;
|
using NicolasDorier.RateLimits;
|
||||||
|
|
||||||
@@ -33,6 +37,8 @@ namespace BTCPayServer.Controllers
|
|||||||
readonly Configuration.BTCPayServerOptions _Options;
|
readonly Configuration.BTCPayServerOptions _Options;
|
||||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||||
private readonly Fido2Service _fido2Service;
|
private readonly Fido2Service _fido2Service;
|
||||||
|
private readonly LnurlAuthService _lnurlAuthService;
|
||||||
|
private readonly LinkGenerator _linkGenerator;
|
||||||
private readonly UserLoginCodeService _userLoginCodeService;
|
private readonly UserLoginCodeService _userLoginCodeService;
|
||||||
private readonly EventAggregator _eventAggregator;
|
private readonly EventAggregator _eventAggregator;
|
||||||
readonly ILogger _logger;
|
readonly ILogger _logger;
|
||||||
@@ -49,6 +55,8 @@ namespace BTCPayServer.Controllers
|
|||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
Fido2Service fido2Service,
|
Fido2Service fido2Service,
|
||||||
UserLoginCodeService userLoginCodeService,
|
UserLoginCodeService userLoginCodeService,
|
||||||
|
LnurlAuthService lnurlAuthService,
|
||||||
|
LinkGenerator linkGenerator,
|
||||||
Logs logs)
|
Logs logs)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
@@ -58,6 +66,8 @@ namespace BTCPayServer.Controllers
|
|||||||
_Options = options;
|
_Options = options;
|
||||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||||
_fido2Service = fido2Service;
|
_fido2Service = fido2Service;
|
||||||
|
_lnurlAuthService = lnurlAuthService;
|
||||||
|
_linkGenerator = linkGenerator;
|
||||||
_userLoginCodeService = userLoginCodeService;
|
_userLoginCodeService = userLoginCodeService;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_logger = logs.PayServer;
|
_logger = logs.PayServer;
|
||||||
@@ -146,7 +156,8 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fido2Devices = await _fido2Service.HasCredentials(user.Id);
|
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))
|
if (await _userManager.CheckPasswordAsync(user, model.Password))
|
||||||
{
|
{
|
||||||
@@ -165,7 +176,8 @@ namespace BTCPayServer.Controllers
|
|||||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||||
{
|
{
|
||||||
LoginWith2FaViewModel = twoFModel,
|
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
|
else
|
||||||
@@ -229,6 +241,84 @@ namespace BTCPayServer.Controllers
|
|||||||
return null;
|
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")]
|
[HttpPost("/login/fido2")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
@@ -269,6 +359,7 @@ namespace BTCPayServer.Controllers
|
|||||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||||
{
|
{
|
||||||
LoginWithFido2ViewModel = viewModel,
|
LoginWithFido2ViewModel = viewModel,
|
||||||
|
LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(viewModel.RememberMe, user) : null,
|
||||||
LoginWith2FaViewModel = !user.TwoFactorEnabled
|
LoginWith2FaViewModel = !user.TwoFactorEnabled
|
||||||
? null
|
? null
|
||||||
: new LoginWith2faViewModel()
|
: new LoginWith2faViewModel()
|
||||||
@@ -300,6 +391,7 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
|
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
|
||||||
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
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,
|
LoginWith2FaViewModel = model,
|
||||||
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
||||||
|
LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,19 @@ namespace BTCPayServer.Controllers
|
|||||||
model.SharedKey = FormatKey(unformattedKey);
|
model.SharedKey = FormatKey(unformattedKey);
|
||||||
model.AuthenticatorUri = GenerateQrCodeUri(user.Email, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace BTCPayServer.Fido2
|
|||||||
var existingKeys =
|
var existingKeys =
|
||||||
user.Fido2Credentials
|
user.Fido2Credentials
|
||||||
.Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2)
|
.Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2)
|
||||||
.Select(c => c.GetBlob().Descriptor).ToList();
|
.Select(c => c.GetFido2Blob().Descriptor).ToList();
|
||||||
|
|
||||||
// 3. Create options
|
// 3. Create options
|
||||||
var authenticatorSelection = new AuthenticatorSelection
|
var authenticatorSelection = new AuthenticatorSelection
|
||||||
@@ -144,7 +144,7 @@ namespace BTCPayServer.Fido2
|
|||||||
public async Task<bool> HasCredentials(string userId)
|
public async Task<bool> HasCredentials(string userId)
|
||||||
{
|
{
|
||||||
await using var context = _contextFactory.CreateContext();
|
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)
|
public async Task<AssertionOptions> RequestLogin(string userId)
|
||||||
@@ -158,7 +158,7 @@ namespace BTCPayServer.Fido2
|
|||||||
}
|
}
|
||||||
var existingCredentials = user.Fido2Credentials
|
var existingCredentials = user.Fido2Credentials
|
||||||
.Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2)
|
.Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2)
|
||||||
.Select(c => c.GetBlob().Descriptor)
|
.Select(c => c.GetFido2Blob().Descriptor)
|
||||||
.ToList();
|
.ToList();
|
||||||
var exts = new AuthenticationExtensionsClientInputs()
|
var exts = new AuthenticationExtensionsClientInputs()
|
||||||
{
|
{
|
||||||
@@ -197,7 +197,7 @@ namespace BTCPayServer.Fido2
|
|||||||
|
|
||||||
var credential = user.Fido2Credentials
|
var credential = user.Fido2Credentials
|
||||||
.Where(fido2Credential => fido2Credential.Type is Fido2Credential.CredentialType.FIDO2)
|
.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));
|
.FirstOrDefault(fido2Credential => fido2Credential.Item2.Descriptor.Id.SequenceEqual(response.Id));
|
||||||
if (credential.Item2 is null)
|
if (credential.Item2 is null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace BTCPayServer.Fido2
|
|||||||
{
|
{
|
||||||
public static class Fido2Extensions
|
public static class Fido2Extensions
|
||||||
{
|
{
|
||||||
public static Fido2CredentialBlob GetBlob(this Fido2Credential credential)
|
public static Fido2CredentialBlob GetFido2Blob(this Fido2Credential credential)
|
||||||
{
|
{
|
||||||
var result = credential.Blob == null
|
var result = credential.Blob == null
|
||||||
? new Fido2CredentialBlob()
|
? new Fido2CredentialBlob()
|
||||||
@@ -16,7 +16,7 @@ namespace BTCPayServer.Fido2
|
|||||||
}
|
}
|
||||||
public static bool SetBlob(this Fido2Credential credential, Fido2CredentialBlob descriptor)
|
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);
|
var newBlob = new Serializer(null).ToString(descriptor);
|
||||||
if (original == newBlob)
|
if (original == newBlob)
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ namespace BTCPayServer.Hosting
|
|||||||
});
|
});
|
||||||
services.AddScoped<Fido2Service>();
|
services.AddScoped<Fido2Service>();
|
||||||
services.AddSingleton<UserLoginCodeService>();
|
services.AddSingleton<UserLoginCodeService>();
|
||||||
|
services.AddSingleton<LnurlAuthService>();
|
||||||
var mvcBuilder = services.AddMvc(o =>
|
var mvcBuilder = services.AddMvc(o =>
|
||||||
{
|
{
|
||||||
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ namespace BTCPayServer.Models.AccountViewModels
|
|||||||
{
|
{
|
||||||
public LoginWithFido2ViewModel LoginWithFido2ViewModel { get; set; }
|
public LoginWithFido2ViewModel LoginWithFido2ViewModel { get; set; }
|
||||||
public LoginWith2faViewModel LoginWith2FaViewModel { get; set; }
|
public LoginWith2faViewModel LoginWith2FaViewModel { get; set; }
|
||||||
|
public LoginWithLNURLAuthViewModel LoginWithLNURLAuthViewModel { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace BTCPayServer.Plugins.Shopify
|
|||||||
|
|
||||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
public class ShopifyController : Controller
|
public class UIShopifyController : Controller
|
||||||
{
|
{
|
||||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||||
private readonly IOptions<BTCPayServerOptions> _btcPayServerOptions;
|
private readonly IOptions<BTCPayServerOptions> _btcPayServerOptions;
|
||||||
@@ -42,7 +42,7 @@ namespace BTCPayServer.Plugins.Shopify
|
|||||||
private readonly IJsonHelper _jsonHelper;
|
private readonly IJsonHelper _jsonHelper;
|
||||||
private readonly IHttpClientFactory _clientFactory;
|
private readonly IHttpClientFactory _clientFactory;
|
||||||
|
|
||||||
public ShopifyController(BTCPayServerEnvironment btcPayServerEnvironment,
|
public UIShopifyController(BTCPayServerEnvironment btcPayServerEnvironment,
|
||||||
IOptions<BTCPayServerOptions> btcPayServerOptions,
|
IOptions<BTCPayServerOptions> btcPayServerOptions,
|
||||||
IWebHostEnvironment webHostEnvironment,
|
IWebHostEnvironment webHostEnvironment,
|
||||||
StoreRepository storeRepository,
|
StoreRepository storeRepository,
|
||||||
@@ -33,14 +33,14 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
|||||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
public class MoneroLikeStoreController : Controller
|
public class UIMoneroLikeStoreController : Controller
|
||||||
{
|
{
|
||||||
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
|
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
|
||||||
private readonly StoreRepository _StoreRepository;
|
private readonly StoreRepository _StoreRepository;
|
||||||
private readonly MoneroRPCProvider _MoneroRpcProvider;
|
private readonly MoneroRPCProvider _MoneroRpcProvider;
|
||||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||||
|
|
||||||
public MoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration,
|
public UIMoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration,
|
||||||
StoreRepository storeRepository, MoneroRPCProvider moneroRpcProvider,
|
StoreRepository storeRepository, MoneroRPCProvider moneroRpcProvider,
|
||||||
BTCPayNetworkProvider btcPayNetworkProvider)
|
BTCPayNetworkProvider btcPayNetworkProvider)
|
||||||
{
|
{
|
||||||
|
|||||||
64
BTCPayServer/Views/LNURLAuth/Create.cshtml
Normal file
64
BTCPayServer/Views/LNURLAuth/Create.cshtml
Normal 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>
|
||||||
2
BTCPayServer/Views/LNURLAuth/_ViewImports.cshtml
Normal file
2
BTCPayServer/Views/LNURLAuth/_ViewImports.cshtml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
|
@using BTCPayServer.Views.Manage
|
||||||
6
BTCPayServer/Views/LNURLAuth/_ViewStart.cshtml
Normal file
6
BTCPayServer/Views/LNURLAuth/_ViewStart.cshtml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@{
|
||||||
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
ViewBag.MainTitle = "Manage your account";
|
||||||
|
|
||||||
|
ViewData["NavPartialName"] = "../UIManage/_Nav";
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
@inject MoneroLikeConfiguration MoneroLikeConfiguration;
|
@inject MoneroLikeConfiguration MoneroLikeConfiguration;
|
||||||
@{
|
@{
|
||||||
var controller = ViewContext.RouteData.Values["Controller"].ToString();
|
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())
|
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
Enabled
|
Enabled
|
||||||
</span>
|
</span>
|
||||||
<span class="text-light ms-3 me-2">|</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
|
Modify
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<span class="me-2 btcpay-status btcpay-status--disabled"></span>
|
<span class="me-2 btcpay-status btcpay-status--disabled"></span>
|
||||||
Disabled
|
Disabled
|
||||||
</span>
|
</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
|
Setup
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<li class="nav-item">
|
<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"/>
|
<vc:icon symbol="shopify"/>
|
||||||
<span>Shopify</span>
|
<span>Shopify</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
72
BTCPayServer/Views/UIAccount/LoginWithLNURLAuth.cshtml
Normal file
72
BTCPayServer/Views/UIAccount/LoginWithLNURLAuth.cshtml
Normal 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>
|
||||||
@@ -3,18 +3,40 @@
|
|||||||
ViewData["Title"] = "Two-factor/U2F authentication";
|
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>
|
<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="row">
|
||||||
<div class="col-lg-12 section-heading">
|
<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">
|
<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>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@model Fido2NetLib.CredentialCreateOptions
|
@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">
|
<form asp-action="CreateResponse" id="registerForm">
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Microsoft Authenticator for
|
Microsoft Authenticator for
|
||||||
<a href="https://go.microsoft.com/fwlink/?Linkid=825072" rel="noreferrer noopener">Android</a> or
|
<a href="https://play.google.com/store/apps/details?id=com.azure.authenticator" rel="noreferrer noopener">Android</a> or
|
||||||
<a href="https://go.microsoft.com/fwlink/?Linkid=825073" rel="noreferrer noopener">iOS</a>
|
<a href="https://itunes.apple.com/us/app/microsoft-authenticator/id983156458" rel="noreferrer noopener">iOS</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Google Authenticator for
|
Google Authenticator for
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ namespace BTCPayServer.Views.Manage
|
|||||||
{
|
{
|
||||||
public enum ManageNavPages
|
public enum ManageNavPages
|
||||||
{
|
{
|
||||||
Index, ChangePassword, TwoFactorAuthentication, APIKeys, Notifications, Fido2, LoginCodes
|
Index, ChangePassword, TwoFactorAuthentication, APIKeys, Notifications, LoginCodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@using Fido2NetLib
|
||||||
@model TwoFactorAuthenticationViewModel
|
@model TwoFactorAuthenticationViewModel
|
||||||
@{
|
@{
|
||||||
ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, "Two-Factor Authentication");
|
ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, "Two-Factor Authentication");
|
||||||
@@ -102,20 +103,39 @@
|
|||||||
{
|
{
|
||||||
var name = string.IsNullOrEmpty(device.Name) ? "Unnamed security device" : device.Name;
|
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">
|
<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">
|
||||||
<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>
|
<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>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<form asp-controller="UIFido2" asp-action="Create" method="get">
|
<form asp-action="CreateCredential">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" name="Name" placeholder="Security device name"/>
|
<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>
|
<span class="fa fa-plus"></span>
|
||||||
Add
|
Add
|
||||||
<span class="d-none d-md-inline-block">Device</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@using BTCPayServer.Views.Stores
|
@using BTCPayServer.Views.Stores
|
||||||
@using BTCPayServer.Abstractions.Extensions
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
@model BTCPayServer.Services.Altcoins.Monero.UI.MoneroLikeStoreController.MoneroLikePaymentMethodViewModel
|
@model BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodViewModel
|
||||||
|
|
||||||
@{
|
@{
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@using BTCPayServer.Views.Stores
|
@using BTCPayServer.Views.Stores
|
||||||
@using BTCPayServer.Abstractions.Extensions
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
@model BTCPayServer.Services.Altcoins.Monero.UI.MoneroLikeStoreController.MoneroLikePaymentMethodListViewModel
|
@model BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodListViewModel
|
||||||
|
|
||||||
@{
|
@{
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
|||||||
@@ -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 > Checkout > Order Processing > Additional Scripts</a>
|
In Shopify please paste following script at <a href="@shopifyUrl/admin/settings/checkout#PolarisTextField1" target="_blank" class="fw-bold" rel="noreferrer noopener"> Settings > Checkout > Order Processing > Additional Scripts</a>
|
||||||
</p>
|
</p>
|
||||||
<kbd style="display: block; word-break: break-all;">
|
<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>
|
</kbd>
|
||||||
</div>
|
</div>
|
||||||
<p class="alert alert-warning">
|
<p class="alert alert-warning">
|
||||||
|
|||||||
@@ -189,7 +189,7 @@
|
|||||||
|
|
||||||
<h4 class="mt-5 mb-3">Default Currency Pairs</h4>
|
<h4 class="mt-5 mb-3">Default Currency Pairs</h4>
|
||||||
<div class="form-group">
|
<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" />
|
<input asp-for="DefaultCurrencyPairs" class="form-control" placeholder="BTC_USD, BTC_CAD" />
|
||||||
<span asp-validation-for="DefaultCurrencyPairs" class="text-danger"></span>
|
<span asp-validation-for="DefaultCurrencyPairs" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user