mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Support LNURL Auth
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
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.Globalization;
|
||||
using System.Security.Claims;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
@@ -17,7 +19,9 @@ using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
@@ -33,6 +37,8 @@ namespace BTCPayServer.Controllers
|
||||
readonly Configuration.BTCPayServerOptions _Options;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
private readonly Fido2Service _fido2Service;
|
||||
private readonly LnurlAuthService _lnurlAuthService;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly UserLoginCodeService _userLoginCodeService;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
readonly ILogger _logger;
|
||||
@@ -49,6 +55,8 @@ namespace BTCPayServer.Controllers
|
||||
EventAggregator eventAggregator,
|
||||
Fido2Service fido2Service,
|
||||
UserLoginCodeService userLoginCodeService,
|
||||
LnurlAuthService lnurlAuthService,
|
||||
LinkGenerator linkGenerator,
|
||||
Logs logs)
|
||||
{
|
||||
_userManager = userManager;
|
||||
@@ -58,6 +66,8 @@ namespace BTCPayServer.Controllers
|
||||
_Options = options;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_fido2Service = fido2Service;
|
||||
_lnurlAuthService = lnurlAuthService;
|
||||
_linkGenerator = linkGenerator;
|
||||
_userLoginCodeService = userLoginCodeService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logs.PayServer;
|
||||
@@ -146,7 +156,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var fido2Devices = await _fido2Service.HasCredentials(user.Id);
|
||||
if (!await _userManager.IsLockedOutAsync(user) && fido2Devices)
|
||||
var lnurlAuthCredentials = await _lnurlAuthService.HasCredentials(user.Id);
|
||||
if (!await _userManager.IsLockedOutAsync(user) && (fido2Devices || lnurlAuthCredentials))
|
||||
{
|
||||
if (await _userManager.CheckPasswordAsync(user, model.Password))
|
||||
{
|
||||
@@ -165,7 +176,8 @@ namespace BTCPayServer.Controllers
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWith2FaViewModel = twoFModel,
|
||||
LoginWithFido2ViewModel = fido2Devices ? await BuildFido2ViewModel(model.RememberMe, user) : null,
|
||||
LoginWithFido2ViewModel = fido2Devices? await BuildFido2ViewModel(model.RememberMe, user): null,
|
||||
LoginWithLNURLAuthViewModel = lnurlAuthCredentials? await BuildLNURLAuthViewModel(model.RememberMe, user): null,
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -229,6 +241,84 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private async Task<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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -6,5 +6,6 @@ namespace BTCPayServer.Models.AccountViewModels
|
||||
{
|
||||
public LoginWithFido2ViewModel LoginWithFido2ViewModel { 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(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,
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
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;
|
||||
@{
|
||||
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())
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
@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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,6 @@ namespace BTCPayServer.Views.Manage
|
||||
{
|
||||
public enum ManageNavPages
|
||||
{
|
||||
Index, ChangePassword, TwoFactorAuthentication, APIKeys, Notifications, Fido2, LoginCodes
|
||||
Index, ChangePassword, TwoFactorAuthentication, APIKeys, Notifications, LoginCodes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
</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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user