mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
FIDO2/WebAuthN Support (#2356)
* FIDO2/WebAuthN Support This adds initial support for WebAuthN/FIDO2 as another MFA mode. U2F is still intact and runs alongside it for now. Once this is merged, I will start work on migrating U2F support to happen over the FIDO2 protocol instead. * Refactor and future proof system (prep work of seamless u2f migration) * attempt js fix for mobile devices * Apply suggestions from code review Co-authored-by: d11n <mail@dennisreimann.de> * fix fido name saving * do not spam logs and hide loader when failed * PR Changes * Apply suggestions from code review Co-authored-by: d11n <mail@dennisreimann.de> * attempt fido2 bump * add name if not named for credentials Co-authored-by: d11n <mail@dennisreimann.de>
This commit is contained in:
@@ -55,6 +55,7 @@ namespace BTCPayServer.Data
|
|||||||
public DbSet<StoreWebhookData> StoreWebhooks { get; set; }
|
public DbSet<StoreWebhookData> StoreWebhooks { get; set; }
|
||||||
public DbSet<StoreData> Stores { get; set; }
|
public DbSet<StoreData> Stores { get; set; }
|
||||||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||||
|
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
|
||||||
public DbSet<UserStore> UserStore { get; set; }
|
public DbSet<UserStore> UserStore { get; set; }
|
||||||
public DbSet<WalletData> Wallets { get; set; }
|
public DbSet<WalletData> Wallets { get; set; }
|
||||||
public DbSet<WalletTransactionData> WalletTransactions { get; set; }
|
public DbSet<WalletTransactionData> WalletTransactions { get; set; }
|
||||||
@@ -99,6 +100,7 @@ namespace BTCPayServer.Data
|
|||||||
StoreWebhookData.OnModelCreating(builder);
|
StoreWebhookData.OnModelCreating(builder);
|
||||||
//StoreData.OnModelCreating(builder);
|
//StoreData.OnModelCreating(builder);
|
||||||
U2FDevice.OnModelCreating(builder);
|
U2FDevice.OnModelCreating(builder);
|
||||||
|
Fido2Credential.OnModelCreating(builder);
|
||||||
Data.UserStore.OnModelCreating(builder);
|
Data.UserStore.OnModelCreating(builder);
|
||||||
//WalletData.OnModelCreating(builder);
|
//WalletData.OnModelCreating(builder);
|
||||||
WalletTransactionData.OnModelCreating(builder);
|
WalletTransactionData.OnModelCreating(builder);
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ namespace BTCPayServer.Data
|
|||||||
|
|
||||||
public List<NotificationData> Notifications { get; set; }
|
public List<NotificationData> Notifications { get; set; }
|
||||||
public List<UserStore> UserStores { get; set; }
|
public List<UserStore> UserStores { get; set; }
|
||||||
|
public List<Fido2Credential> Fido2Credentials { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
BTCPayServer.Data/Data/Fido2Credential.cs
Normal file
33
BTCPayServer.Data/Data/Fido2Credential.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data
|
||||||
|
{
|
||||||
|
public class Fido2Credential
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string ApplicationUserId { get; set; }
|
||||||
|
|
||||||
|
public byte[] Blob { get; set; }
|
||||||
|
public CredentialType Type { get; set; }
|
||||||
|
public enum CredentialType
|
||||||
|
{
|
||||||
|
FIDO2,
|
||||||
|
U2F
|
||||||
|
}
|
||||||
|
public static void OnModelCreating(ModelBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Entity<Fido2Credential>()
|
||||||
|
.HasOne(o => o.ApplicationUser)
|
||||||
|
.WithMany(i => i.Fido2Credentials)
|
||||||
|
.HasForeignKey(i => i.ApplicationUserId).OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApplicationUser ApplicationUser { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20210314092253_Fido2Credentials")]
|
||||||
|
public partial class Fido2Credentials : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Fido2Credentials",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(nullable: false),
|
||||||
|
Name = table.Column<string>(nullable: true),
|
||||||
|
ApplicationUserId = table.Column<string>(nullable: true),
|
||||||
|
Blob = table.Column<byte[]>(nullable: true),
|
||||||
|
Type = table.Column<int>(nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Fido2Credentials", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Fido2Credentials_AspNetUsers_ApplicationUserId",
|
||||||
|
column: x => x.ApplicationUserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Fido2Credentials_ApplicationUserId",
|
||||||
|
table: "Fido2Credentials",
|
||||||
|
column: "ApplicationUserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Fido2Credentials");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -170,6 +170,31 @@ namespace BTCPayServer.Migrations
|
|||||||
b.ToTable("AspNetUsers");
|
b.ToTable("AspNetUsers");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ApplicationUserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ApplicationUserId");
|
||||||
|
|
||||||
|
b.ToTable("Fido2Credentials");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("InvoiceDataId")
|
b.Property<string>("InvoiceDataId")
|
||||||
@@ -958,6 +983,14 @@ namespace BTCPayServer.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||||
|
.WithMany("Fido2Credentials")
|
||||||
|
.HasForeignKey("ApplicationUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
|||||||
@@ -53,4 +53,8 @@
|
|||||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Pages" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
||||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
||||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||||
|
<PackageReference Include="Fido2" Version="2.0.0-preview2" />
|
||||||
|
<PackageReference Include="Fido2.AspNet" Version="2.0.0-preview2" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||||
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.3.1" />
|
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.3.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ else
|
|||||||
if (!disabled)
|
if (!disabled)
|
||||||
{
|
{
|
||||||
var user = await UserManager.GetUserAsync(User);
|
var user = await UserManager.GetUserAsync(User);
|
||||||
disabled = user.DisabledNotifications == "all";
|
disabled = user?.DisabledNotifications == "all";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (!disabled)
|
@if (!disabled)
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Security.Policy;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Events;
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.Fido2;
|
||||||
|
using BTCPayServer.Fido2.Models;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.Models;
|
|
||||||
using BTCPayServer.Models.AccountViewModels;
|
using BTCPayServer.Models.AccountViewModels;
|
||||||
using BTCPayServer.Security;
|
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.U2F;
|
using BTCPayServer.U2F;
|
||||||
using BTCPayServer.U2F.Models;
|
using BTCPayServer.U2F.Models;
|
||||||
|
using Fido2NetLib;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
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.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using NicolasDorier.RateLimits;
|
using NicolasDorier.RateLimits;
|
||||||
using U2F.Core.Exceptions;
|
using U2F.Core.Exceptions;
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ namespace BTCPayServer.Controllers
|
|||||||
readonly Configuration.BTCPayServerOptions _Options;
|
readonly Configuration.BTCPayServerOptions _Options;
|
||||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||||
public U2FService _u2FService;
|
public U2FService _u2FService;
|
||||||
private readonly RateLimitService _rateLimitService;
|
private readonly Fido2Service _fido2Service;
|
||||||
private readonly EventAggregator _eventAggregator;
|
private readonly EventAggregator _eventAggregator;
|
||||||
readonly ILogger _logger;
|
readonly ILogger _logger;
|
||||||
|
|
||||||
@@ -47,8 +48,8 @@ namespace BTCPayServer.Controllers
|
|||||||
Configuration.BTCPayServerOptions options,
|
Configuration.BTCPayServerOptions options,
|
||||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||||
U2FService u2FService,
|
U2FService u2FService,
|
||||||
RateLimitService rateLimitService,
|
EventAggregator eventAggregator,
|
||||||
EventAggregator eventAggregator)
|
Fido2Service fido2Service)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_signInManager = signInManager;
|
_signInManager = signInManager;
|
||||||
@@ -57,7 +58,7 @@ namespace BTCPayServer.Controllers
|
|||||||
_Options = options;
|
_Options = options;
|
||||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||||
_u2FService = u2FService;
|
_u2FService = u2FService;
|
||||||
_rateLimitService = rateLimitService;
|
_fido2Service = fido2Service;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_logger = Logs.PayServer;
|
_logger = Logs.PayServer;
|
||||||
}
|
}
|
||||||
@@ -125,7 +126,9 @@ namespace BTCPayServer.Controllers
|
|||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id))
|
var u2fDevices = await _u2FService.HasDevices(user.Id);
|
||||||
|
var fido2Devices = await _fido2Service.HasCredentials(user.Id);
|
||||||
|
if (!await _userManager.IsLockedOutAsync(user) && u2fDevices || fido2Devices)
|
||||||
{
|
{
|
||||||
if (await _userManager.CheckPasswordAsync(user, model.Password))
|
if (await _userManager.CheckPasswordAsync(user, model.Password))
|
||||||
{
|
{
|
||||||
@@ -144,7 +147,8 @@ namespace BTCPayServer.Controllers
|
|||||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||||
{
|
{
|
||||||
LoginWith2FaViewModel = twoFModel,
|
LoginWith2FaViewModel = twoFModel,
|
||||||
LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user)
|
LoginWithU2FViewModel = u2fDevices? await BuildU2FViewModel(model.RememberMe, user) : null,
|
||||||
|
LoginWithFido2ViewModel = fido2Devices? await BuildFido2ViewModel(model.RememberMe, user): null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -210,6 +214,77 @@ namespace BTCPayServer.Controllers
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<LoginWithFido2ViewModel> BuildFido2ViewModel(bool rememberMe, ApplicationUser user)
|
||||||
|
{
|
||||||
|
if (_btcPayServerEnvironment.IsSecure)
|
||||||
|
{
|
||||||
|
var r = await _fido2Service.RequestLogin(user.Id);
|
||||||
|
if (r is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new LoginWithFido2ViewModel()
|
||||||
|
{
|
||||||
|
Data = r,
|
||||||
|
UserId = user.Id,
|
||||||
|
RememberMe = rememberMe
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> LoginWithFido2(LoginWithFido2ViewModel viewModel, string returnUrl = null)
|
||||||
|
{
|
||||||
|
if (!CanLoginOrRegister())
|
||||||
|
{
|
||||||
|
return RedirectToAction("Login");
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewData["ReturnUrl"] = returnUrl;
|
||||||
|
var user = await _userManager.FindByIdAsync(viewModel.UserId);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorMessage = string.Empty;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (await _fido2Service.CompleteLogin(viewModel.UserId, JObject.Parse(viewModel.Response).ToObject<AuthenticatorAssertionRawResponse>()))
|
||||||
|
{
|
||||||
|
await _signInManager.SignInAsync(user, viewModel.RememberMe, "FIDO2");
|
||||||
|
_logger.LogInformation("User logged in.");
|
||||||
|
return RedirectToLocal(returnUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage = "Invalid login attempt.";
|
||||||
|
}
|
||||||
|
catch (U2fException e)
|
||||||
|
{
|
||||||
|
errorMessage = e.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelState.AddModelError(string.Empty, errorMessage);
|
||||||
|
viewModel.Response = null;
|
||||||
|
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||||
|
{
|
||||||
|
LoginWithFido2ViewModel = viewModel,
|
||||||
|
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(viewModel.RememberMe, user) : null,
|
||||||
|
LoginWith2FaViewModel = !user.TwoFactorEnabled
|
||||||
|
? null
|
||||||
|
: new LoginWith2faViewModel()
|
||||||
|
{
|
||||||
|
RememberMe = viewModel.RememberMe
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
@@ -280,7 +355,8 @@ namespace BTCPayServer.Controllers
|
|||||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||||
{
|
{
|
||||||
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
|
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
|
||||||
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
|
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null,
|
||||||
|
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +402,8 @@ namespace BTCPayServer.Controllers
|
|||||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||||
{
|
{
|
||||||
LoginWith2FaViewModel = model,
|
LoginWith2FaViewModel = model,
|
||||||
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
|
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null,
|
||||||
|
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
104
BTCPayServer/Fido2/Fido2Controller.cs
Normal file
104
BTCPayServer/Fido2/Fido2Controller.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
|
using BTCPayServer.Abstractions.Models;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Fido2;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using Fido2NetLib;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.U2F.Models
|
||||||
|
{
|
||||||
|
|
||||||
|
[Route("fido2")]
|
||||||
|
[Authorize]
|
||||||
|
public class Fido2Controller : Controller
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly Fido2Service _fido2Service;
|
||||||
|
|
||||||
|
public Fido2Controller(UserManager<ApplicationUser> userManager, Fido2Service fido2Service)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_fido2Service = fido2Service;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("")]
|
||||||
|
public async Task<IActionResult> List()
|
||||||
|
{
|
||||||
|
return View(new Fido2AuthenticationViewModel()
|
||||||
|
{
|
||||||
|
Credentials = await _fido2Service.GetCredentials( _userManager.GetUserId(User))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}/delete")]
|
||||||
|
public IActionResult Remove(string id)
|
||||||
|
{
|
||||||
|
return View("Confirm", new ConfirmModel("Are you sure you want to remove FIDO2 credential?", "Your account will no longer have this credential as an option for MFA.", "Remove"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}/delete")]
|
||||||
|
public async Task<IActionResult> RemoveP(string id)
|
||||||
|
{
|
||||||
|
|
||||||
|
await _fido2Service.Remove(id, _userManager.GetUserId(User));
|
||||||
|
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||||
|
Html = $"FIDO2 Credentials were removed successfully."
|
||||||
|
});
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(List));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("register")]
|
||||||
|
public async Task<IActionResult> Create(AddFido2CredentialViewModel viewModel)
|
||||||
|
{
|
||||||
|
var options = await _fido2Service.RequestCreation(_userManager.GetUserId(User));
|
||||||
|
if (options is null)
|
||||||
|
{
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||||
|
Html = $"FIDO2 Credentials could not be saved."
|
||||||
|
});
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(List));
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewData["CredentialName"] = viewModel.Name ?? "";
|
||||||
|
return View(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("register")]
|
||||||
|
public async Task<IActionResult> CreateResponse([FromForm] string data, [FromForm] string name)
|
||||||
|
{
|
||||||
|
var attestationResponse = JObject.Parse(data).ToObject<AuthenticatorAttestationRawResponse>();
|
||||||
|
if (await _fido2Service.CompleteCreation(_userManager.GetUserId(User), name, attestationResponse))
|
||||||
|
{
|
||||||
|
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||||
|
Html = $"FIDO2 Credentials were saved successfully."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||||
|
Html = $"FIDO2 Credentials could not be saved."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(List));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
206
BTCPayServer/Fido2/Fido2Service.cs
Normal file
206
BTCPayServer/Fido2/Fido2Service.cs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using ExchangeSharp;
|
||||||
|
using Fido2NetLib;
|
||||||
|
using Fido2NetLib.Objects;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Fido2
|
||||||
|
{
|
||||||
|
public class Fido2Service
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<string, CredentialCreateOptions> CreationStore =
|
||||||
|
new ConcurrentDictionary<string, CredentialCreateOptions>();
|
||||||
|
private static readonly ConcurrentDictionary<string, AssertionOptions> LoginStore =
|
||||||
|
new ConcurrentDictionary<string, AssertionOptions>();
|
||||||
|
private readonly ApplicationDbContextFactory _contextFactory;
|
||||||
|
private readonly IFido2 _fido2;
|
||||||
|
|
||||||
|
public Fido2Service(ApplicationDbContextFactory contextFactory, IFido2 fido2)
|
||||||
|
{
|
||||||
|
_contextFactory = contextFactory;
|
||||||
|
_fido2 = fido2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CredentialCreateOptions> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get user existing keys by username
|
||||||
|
var existingKeys =
|
||||||
|
user.Fido2Credentials
|
||||||
|
.Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2)
|
||||||
|
.Select(c => c.GetBlob().Descriptor).ToList();
|
||||||
|
|
||||||
|
// 3. Create options
|
||||||
|
var authenticatorSelection = new AuthenticatorSelection
|
||||||
|
{
|
||||||
|
RequireResidentKey = false, UserVerification = UserVerificationRequirement.Preferred
|
||||||
|
};
|
||||||
|
|
||||||
|
var exts = new AuthenticationExtensionsClientInputs()
|
||||||
|
{
|
||||||
|
Extensions = true,
|
||||||
|
UserVerificationIndex = true,
|
||||||
|
Location = true,
|
||||||
|
UserVerificationMethod = true,
|
||||||
|
BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds
|
||||||
|
{
|
||||||
|
FAR = float.MaxValue, FRR = float.MaxValue
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = _fido2.RequestNewCredential(
|
||||||
|
new Fido2User() {DisplayName = user.UserName, Name = user.UserName, Id = user.Id.ToBytesUTF8()},
|
||||||
|
existingKeys, authenticatorSelection, AttestationConveyancePreference.None, exts);
|
||||||
|
|
||||||
|
// options.Rp = new PublicKeyCredentialRpEntity(Request.Host.Host, options.Rp.Name, "");
|
||||||
|
CreationStore.AddOrReplace(userId, options);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CompleteCreation(string userId, string name, AuthenticatorAttestationRawResponse attestationResponse)
|
||||||
|
{
|
||||||
|
await using var dbContext = _contextFactory.CreateContext();
|
||||||
|
var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials)
|
||||||
|
.FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId);
|
||||||
|
if (user == null || !CreationStore.TryGetValue(userId, out var options))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Verify and make the credentials
|
||||||
|
var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, args => Task.FromResult(true));
|
||||||
|
|
||||||
|
// 3. Store the credentials in db
|
||||||
|
var newCredential = new Fido2Credential()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
ApplicationUserId = userId
|
||||||
|
};
|
||||||
|
|
||||||
|
newCredential.SetBlob(new Fido2CredentialBlob()
|
||||||
|
{
|
||||||
|
Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
|
||||||
|
PublicKey = success.Result.PublicKey,
|
||||||
|
UserHandle = success.Result.User.Id,
|
||||||
|
SignatureCounter = success.Result.Counter,
|
||||||
|
CredType = success.Result.CredType,
|
||||||
|
AaGuid = success.Result.Aaguid.ToString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await dbContext.Fido2Credentials.AddAsync(newCredential);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
CreationStore.Remove(userId, out _);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Fido2Credential>> GetCredentials(string userId)
|
||||||
|
{
|
||||||
|
await using var context = _contextFactory.CreateContext();
|
||||||
|
return await context.Fido2Credentials
|
||||||
|
.Where(device => device.ApplicationUserId == userId)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<bool> HasCredentials(string userId)
|
||||||
|
{
|
||||||
|
await using var context = _contextFactory.CreateContext();
|
||||||
|
return await context.Fido2Credentials.Where(fDevice => fDevice.ApplicationUserId == userId).AnyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AssertionOptions> 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() is true))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var existingCredentials = user.Fido2Credentials
|
||||||
|
.Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2)
|
||||||
|
.Select(c => c.GetBlob().Descriptor)
|
||||||
|
.ToList();
|
||||||
|
var exts = new AuthenticationExtensionsClientInputs()
|
||||||
|
{
|
||||||
|
SimpleTransactionAuthorization = "FIDO",
|
||||||
|
GenericTransactionAuthorization = new TxAuthGenericArg
|
||||||
|
{
|
||||||
|
ContentType = "text/plain",
|
||||||
|
Content = new byte[] { 0x46, 0x49, 0x44, 0x4F }
|
||||||
|
},
|
||||||
|
UserVerificationIndex = true,
|
||||||
|
Location = true,
|
||||||
|
UserVerificationMethod = true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Create options
|
||||||
|
var options = _fido2.GetAssertionOptions(
|
||||||
|
existingCredentials,
|
||||||
|
UserVerificationRequirement.Discouraged,
|
||||||
|
exts
|
||||||
|
);
|
||||||
|
LoginStore.AddOrReplace(userId, options);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CompleteLogin(string userId, AuthenticatorAssertionRawResponse response){
|
||||||
|
await using var dbContext = _contextFactory.CreateContext();
|
||||||
|
var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials)
|
||||||
|
.FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId);
|
||||||
|
if (user == null || !LoginStore.TryGetValue(userId, out var options))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var credential = user.Fido2Credentials
|
||||||
|
.Where(fido2Credential => fido2Credential.Type is Fido2Credential.CredentialType.FIDO2)
|
||||||
|
.Select(fido2Credential => (fido2Credential, fido2Credential.GetBlob()))
|
||||||
|
.FirstOrDefault(fido2Credential => fido2Credential.Item2.Descriptor.Id.SequenceEqual(response.Id));
|
||||||
|
if (credential.Item2 is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Make the assertion
|
||||||
|
var res = await _fido2.MakeAssertionAsync(response, options, credential.Item2.PublicKey,
|
||||||
|
credential.Item2.SignatureCounter, x => Task.FromResult(true));
|
||||||
|
|
||||||
|
// 6. Store the updated counter
|
||||||
|
credential.Item2.SignatureCounter = res.Counter;
|
||||||
|
credential.fido2Credential.SetBlob(credential.Item2);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
LoginStore.Remove(userId, out _);
|
||||||
|
|
||||||
|
// 7. return OK to client
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
BTCPayServer/Fido2/FidoExtensions.cs
Normal file
30
BTCPayServer/Fido2/FidoExtensions.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using NBXplorer;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data
|
||||||
|
{
|
||||||
|
public static class Fido2Extensions
|
||||||
|
{
|
||||||
|
public static Fido2CredentialBlob GetBlob(this Fido2Credential credential)
|
||||||
|
{
|
||||||
|
var result = credential.Blob == null
|
||||||
|
? new Fido2CredentialBlob()
|
||||||
|
: JObject.Parse(ZipUtils.Unzip(credential.Blob)).ToObject<Fido2CredentialBlob>();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
public static bool SetBlob(this Fido2Credential credential, Fido2CredentialBlob descriptor)
|
||||||
|
{
|
||||||
|
var original = new Serializer(null).ToString(credential.GetBlob());
|
||||||
|
var newBlob = new Serializer(null).ToString(descriptor);
|
||||||
|
if (original == newBlob)
|
||||||
|
return false;
|
||||||
|
credential.Type = Fido2Credential.CredentialType.FIDO2;
|
||||||
|
credential.Blob = ZipUtils.Zip(newBlob);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
11
BTCPayServer/Fido2/Models/AddFido2CredentialViewModel.cs
Normal file
11
BTCPayServer/Fido2/Models/AddFido2CredentialViewModel.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Fido2NetLib.Objects;
|
||||||
|
|
||||||
|
namespace BTCPayServer.U2F.Models
|
||||||
|
{
|
||||||
|
public class AddFido2CredentialViewModel
|
||||||
|
{
|
||||||
|
public AuthenticatorAttachment? AuthenticatorAttachment { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
10
BTCPayServer/Fido2/Models/Fido2AuthenticationViewModel.cs
Normal file
10
BTCPayServer/Fido2/Models/Fido2AuthenticationViewModel.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
|
||||||
|
namespace BTCPayServer.U2F.Models
|
||||||
|
{
|
||||||
|
public class Fido2AuthenticationViewModel
|
||||||
|
{
|
||||||
|
public List<Fido2Credential> Credentials { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
18
BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs
Normal file
18
BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using Fido2NetLib;
|
||||||
|
using Fido2NetLib.Objects;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data
|
||||||
|
{
|
||||||
|
public class Fido2CredentialBlob
|
||||||
|
{
|
||||||
|
public PublicKeyCredentialDescriptor Descriptor { get; set; }
|
||||||
|
[JsonConverter(typeof(Base64UrlConverter))]
|
||||||
|
public byte[] PublicKey { get; set; }
|
||||||
|
[JsonConverter(typeof(Base64UrlConverter))]
|
||||||
|
public byte[] UserHandle { get; set; }
|
||||||
|
public uint SignatureCounter { get; set; }
|
||||||
|
public string CredType { get; set; }
|
||||||
|
public string AaGuid { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
BTCPayServer/Fido2/Models/LoginWithFido2ViewModel.cs
Normal file
13
BTCPayServer/Fido2/Models/LoginWithFido2ViewModel.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Fido2NetLib;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Fido2.Models
|
||||||
|
{
|
||||||
|
public class LoginWithFido2ViewModel
|
||||||
|
{
|
||||||
|
public string UserId { get; set; }
|
||||||
|
|
||||||
|
public bool RememberMe { get; set; }
|
||||||
|
public AssertionOptions Data { get; set; }
|
||||||
|
public string Response { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Fido2;
|
||||||
using BTCPayServer.Filters;
|
using BTCPayServer.Filters;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.PaymentRequest;
|
using BTCPayServer.PaymentRequest;
|
||||||
@@ -12,9 +14,11 @@ using BTCPayServer.Services.Apps;
|
|||||||
using BTCPayServer.Storage;
|
using BTCPayServer.Storage;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Fido2NetLib;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
@@ -78,6 +82,29 @@ namespace BTCPayServer.Hosting
|
|||||||
services.AddProviderStorage();
|
services.AddProviderStorage();
|
||||||
services.AddSession();
|
services.AddSession();
|
||||||
services.AddSignalR();
|
services.AddSignalR();
|
||||||
|
services.AddFido2(options =>
|
||||||
|
{
|
||||||
|
options.ServerName = "BTCPay Server";
|
||||||
|
})
|
||||||
|
.AddCachedMetadataService(config =>
|
||||||
|
{
|
||||||
|
//They'll be used in a "first match wins" way in the order registered
|
||||||
|
config.AddStaticMetadataRepository();
|
||||||
|
});
|
||||||
|
var descriptor =services.Single(descriptor => descriptor.ServiceType == typeof(Fido2Configuration));
|
||||||
|
services.Remove(descriptor);
|
||||||
|
services.AddScoped(provider =>
|
||||||
|
{
|
||||||
|
var httpContext = provider.GetService<IHttpContextAccessor>();
|
||||||
|
return new Fido2Configuration()
|
||||||
|
{
|
||||||
|
ServerName = "BTCPay Server",
|
||||||
|
Origin = $"{httpContext.HttpContext.Request.Scheme}://{httpContext.HttpContext.Request.Host}",
|
||||||
|
ServerDomain = httpContext.HttpContext.Request.Host.Host
|
||||||
|
};
|
||||||
|
});
|
||||||
|
services.AddScoped<Fido2Service>();
|
||||||
|
|
||||||
var mvcBuilder= services.AddMvc(o =>
|
var mvcBuilder= services.AddMvc(o =>
|
||||||
{
|
{
|
||||||
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
using BTCPayServer.Fido2.Models;
|
||||||
using BTCPayServer.U2F.Models;
|
using BTCPayServer.U2F.Models;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.AccountViewModels
|
namespace BTCPayServer.Models.AccountViewModels
|
||||||
{
|
{
|
||||||
public class SecondaryLoginViewModel
|
public class SecondaryLoginViewModel
|
||||||
{
|
{
|
||||||
|
public LoginWithFido2ViewModel LoginWithFido2ViewModel { get; set; }
|
||||||
public LoginWith2faViewModel LoginWith2FaViewModel { get; set; }
|
public LoginWith2faViewModel LoginWith2FaViewModel { get; set; }
|
||||||
public LoginWithU2FViewModel LoginWithU2FViewModel { get; set; }
|
public LoginWithU2FViewModel LoginWithU2FViewModel { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ namespace BTCPayServer
|
|||||||
l.AddFilter("Microsoft", LogLevel.Error);
|
l.AddFilter("Microsoft", LogLevel.Error);
|
||||||
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
|
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
|
||||||
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
|
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
|
||||||
|
l.AddFilter("Fido2NetLib.DistributedCacheMetadataService", LogLevel.Error);
|
||||||
l.AddProvider(new CustomConsoleLogProvider(processor));
|
l.AddProvider(new CustomConsoleLogProvider(processor));
|
||||||
})
|
})
|
||||||
.UseStartup<Startup>()
|
.UseStartup<Startup>()
|
||||||
|
|||||||
32
BTCPayServer/Views/Account/LoginWithFido2.cshtml
Normal file
32
BTCPayServer/Views/Account/LoginWithFido2.cshtml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@model BTCPayServer.Fido2.Models.LoginWithFido2ViewModel
|
||||||
|
|
||||||
|
<form id="fidoForm" asp-action="LoginWithFido2" method="post" asp-route-returnUrl="@ViewData["ReturnUrl"]">
|
||||||
|
|
||||||
|
<input type="hidden" asp-for="Data"/>
|
||||||
|
<input type="hidden" asp-for="Response"/>
|
||||||
|
<input type="hidden" asp-for="UserId"/>
|
||||||
|
<input type="hidden" asp-for="RememberMe"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<section class="pt-5">
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 section-heading">
|
||||||
|
<h2>FIDO2 Authentication</h2>
|
||||||
|
<hr class="primary">
|
||||||
|
<div>
|
||||||
|
<span id="spinner" class="fa fa-spinner fa-spin float-right ml-3 mr-5 mt-1 fido-running" style="font-size:2.5em"></span>
|
||||||
|
<p>Insert your security key into your computer's USB port. If it has a button, tap on it.</p>
|
||||||
|
</div>
|
||||||
|
<p id="error-message" class="d-none alert alert-danger"></p>
|
||||||
|
<a id="btn-retry" class="btn btn-secondary d-none" href="javascript:window.location.reload()">Retry</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<script>
|
||||||
|
// send to server for registering
|
||||||
|
window.makeAssertionOptions = @Safe.Json(Model.Data);
|
||||||
|
</script>
|
||||||
|
<script src="~/js/webauthn/helpers.js" ></script>
|
||||||
|
<script src="~/js/webauthn/login.js" ></script>
|
||||||
@@ -5,15 +5,15 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@if (Model.LoginWith2FaViewModel != null && Model.LoginWithU2FViewModel != null)
|
@if (Model.LoginWith2FaViewModel != null && Model.LoginWithU2FViewModel != null && Model.LoginWithFido2ViewModel != 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.LoginWithU2FViewModel == null)
|
else if (Model.LoginWith2FaViewModel == null && Model.LoginWithU2FViewModel == null && Model.LoginWithFido2ViewModel == 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 Authentication Methods are not available. Please go to the https endpoint</h2>
|
<h2 class="bg-danger">Both 2FA and U2F/FIDO2 Authentication Methods are not available. Please go to the https endpoint.</h2>
|
||||||
<hr class="danger">
|
<hr class="danger">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,6 +33,12 @@
|
|||||||
<partial name="LoginWithU2F" model="@Model.LoginWithU2FViewModel"/>
|
<partial name="LoginWithU2F" model="@Model.LoginWithU2FViewModel"/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@if (Model.LoginWithFido2ViewModel != null)
|
||||||
|
{
|
||||||
|
<div class="col-sm-12 col-md-6">
|
||||||
|
<partial name="LoginWithFido2" model="@Model.LoginWithFido2ViewModel"/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
25
BTCPayServer/Views/Fido2/Create.cshtml
Normal file
25
BTCPayServer/Views/Fido2/Create.cshtml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
@model Fido2NetLib.CredentialCreateOptions
|
||||||
|
@{
|
||||||
|
ViewData.SetActivePageAndTitle(ManageNavPages.Fido2, "Register FIDO2 Credentials");
|
||||||
|
}
|
||||||
|
<form asp-action="CreateResponse" id="registerForm" >
|
||||||
|
<input type="hidden" name="data" id="data" />
|
||||||
|
<input type="hidden" name="name" id="name" value="@(ViewData.ContainsKey("CredentialName")? ViewData["CredentialName"] : string.Empty)" />
|
||||||
|
</form>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 section-heading">
|
||||||
|
<div>
|
||||||
|
<span id="spinner" class="fa fa-spinner fa-spin float-right ml-3 mr-5 mt-1 fido-running" style="font-size:2.5em"></span>
|
||||||
|
<p>Insert your security key into your computer's USB port. If it has a button, tap on it.</p>
|
||||||
|
</div>
|
||||||
|
<p id="error-message" class="d-none alert alert-danger"></p>
|
||||||
|
<a id="btn-retry" class="btn btn-secondary d-none" href="javascript:window.location.reload()">Retry</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// send to server for registering
|
||||||
|
window.makeCredentialOptions = @Json.Serialize(Model);
|
||||||
|
</script>
|
||||||
|
<script src="~/js/webauthn/helpers.js" ></script>
|
||||||
|
<script src="~/js/webauthn/register.js" ></script>
|
||||||
44
BTCPayServer/Views/Fido2/List.cshtml
Normal file
44
BTCPayServer/Views/Fido2/List.cshtml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
@model BTCPayServer.U2F.Models.Fido2AuthenticationViewModel
|
||||||
|
@{
|
||||||
|
ViewData.SetActivePageAndTitle(ManageNavPages.Fido2, "Registered FIDO2 Credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
<partial name="_StatusMessage"/>
|
||||||
|
|
||||||
|
<table class="table table-lg mb-4">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th class="text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var device in Model.Credentials)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@(string.IsNullOrEmpty(device.Name)? "Unnamed FIDO2 credential": device.Name)</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<a asp-action="Remove" asp-route-id="@device.Id">Remove</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (!Model.Credentials.Any())
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="text-center h5 py-2">
|
||||||
|
No registered credentials
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<form asp-action="Create" method="get" class="form-inline">
|
||||||
|
<div class="form-group">
|
||||||
|
|
||||||
|
<input type="text" class="form-control" name="Name" placeholder="New Credential Name"/>
|
||||||
|
<button type="submit" class="btn btn-primary ml-2">
|
||||||
|
<span class="fa fa-plus"></span>
|
||||||
|
Add New Credential
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
1
BTCPayServer/Views/Fido2/_ViewImports.cshtml
Normal file
1
BTCPayServer/Views/Fido2/_ViewImports.cshtml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@using BTCPayServer.Views.Manage
|
||||||
6
BTCPayServer/Views/Fido2/_ViewStart.cshtml
Normal file
6
BTCPayServer/Views/Fido2/_ViewStart.cshtml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@{
|
||||||
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
ViewBag.MainTitle = "Manage your account";
|
||||||
|
|
||||||
|
ViewData["NavPartialName"] = "../Manage/_Nav";
|
||||||
|
}
|
||||||
@@ -2,6 +2,6 @@ namespace BTCPayServer.Views.Manage
|
|||||||
{
|
{
|
||||||
public enum ManageNavPages
|
public enum ManageNavPages
|
||||||
{
|
{
|
||||||
Index, ChangePassword, TwoFactorAuthentication, U2F, APIKeys, Notifications
|
Index, ChangePassword, TwoFactorAuthentication, U2F, APIKeys, Notifications, Fido2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
@inject SignInManager<ApplicationUser> SignInManager
|
@inject SignInManager<ApplicationUser> SignInManager
|
||||||
|
|
||||||
<div class="nav flex-column mb-4">
|
|
||||||
<a id="@ManageNavPages.Index.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-action="Index">Profile</a>
|
|
||||||
<a id="@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-action="ChangePassword">Password</a>
|
|
||||||
<a id="@ManageNavPages.TwoFactorAuthentication.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
|
|
||||||
<a id="@ManageNavPages.U2F.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.U2F)" asp-action="U2FAuthentication">U2F Authentication</a>
|
|
||||||
<a id="@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-action="APIKeys">API Keys</a>
|
<a id="@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-action="APIKeys">API Keys</a>
|
||||||
<a id="@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-action="NotificationSettings">Notifications</a>
|
<div class="nav flex-column mb-4">
|
||||||
<vc:ui-extension-point location="user-nav" />
|
<a id="@ManageNavPages.Index.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-controller="Manage" asp-action="Index">Profile</a>
|
||||||
</div>
|
<a id="@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-controller="Manage" asp-action="ChangePassword">Password</a>
|
||||||
|
<a id="@ManageNavPages.TwoFactorAuthentication.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-controller="Manage" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
|
||||||
|
<a id="@ManageNavPages.U2F.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.U2F)" asp-controller="Manage" asp-action="U2FAuthentication">U2F Authentication</a>
|
||||||
|
<a id="@ManageNavPages.Fido2.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Fido2)" asp-action="List" asp-controller="Fido2">FIDO2 Authentication</a>
|
||||||
|
<a id="@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-controller="Manage" asp-action="APIKeys">API Keys</a>
|
||||||
|
<a id="@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-controller="Manage" asp-action="NotificationSettings">Notifications</a>
|
||||||
|
<vc:ui-extension-point location="user-nav"/>
|
||||||
|
</div>
|
||||||
|
|||||||
109
BTCPayServer/wwwroot/js/webauthn/helpers.js
Normal file
109
BTCPayServer/wwwroot/js/webauthn/helpers.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
coerceToArrayBuffer = function (thing, name) {
|
||||||
|
if (typeof thing === "string") {
|
||||||
|
// base64url to base64
|
||||||
|
thing = thing.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
|
||||||
|
// base64 to Uint8Array
|
||||||
|
var str = window.atob(thing);
|
||||||
|
var bytes = new Uint8Array(str.length);
|
||||||
|
for (var i = 0; i < str.length; i++) {
|
||||||
|
bytes[i] = str.charCodeAt(i);
|
||||||
|
}
|
||||||
|
thing = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array to Uint8Array
|
||||||
|
if (Array.isArray(thing)) {
|
||||||
|
thing = new Uint8Array(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint8Array to ArrayBuffer
|
||||||
|
if (thing instanceof Uint8Array) {
|
||||||
|
thing = thing.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// error if none of the above worked
|
||||||
|
if (!(thing instanceof ArrayBuffer)) {
|
||||||
|
throw new TypeError("could not coerce '" + name + "' to ArrayBuffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
return thing;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
coerceToBase64Url = function (thing) {
|
||||||
|
// Array or ArrayBuffer to Uint8Array
|
||||||
|
if (Array.isArray(thing)) {
|
||||||
|
thing = Uint8Array.from(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thing instanceof ArrayBuffer) {
|
||||||
|
thing = new Uint8Array(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint8Array to base64
|
||||||
|
if (thing instanceof Uint8Array) {
|
||||||
|
var str = "";
|
||||||
|
var len = thing.byteLength;
|
||||||
|
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
str += String.fromCharCode(thing[i]);
|
||||||
|
}
|
||||||
|
thing = window.btoa(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof thing !== "string") {
|
||||||
|
throw new Error("could not coerce to string");
|
||||||
|
}
|
||||||
|
|
||||||
|
// base64 to base64url
|
||||||
|
// NOTE: "=" at the end of challenge is optional, strip it off here
|
||||||
|
thing = thing.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
|
||||||
|
|
||||||
|
return thing;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// HELPERS
|
||||||
|
|
||||||
|
function showErrorAlert(message, error) {
|
||||||
|
let footermsg = '';
|
||||||
|
if (error) {
|
||||||
|
footermsg = 'exception:' + error.toString();
|
||||||
|
}
|
||||||
|
console.error(message, footermsg);
|
||||||
|
document.getElementById("btn-retry").classList.remove("d-none");
|
||||||
|
document.getElementById("error-message").textContent = message;
|
||||||
|
for(let el of document.getElementsByClassName("fido-running")){
|
||||||
|
el.classList.add("d-none");
|
||||||
|
}
|
||||||
|
document.getElementById("error-message").classList.remove("d-none");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectFIDOSupport() {
|
||||||
|
if (window.PublicKeyCredential === undefined ||
|
||||||
|
typeof window.PublicKeyCredential !== "function") {
|
||||||
|
//$('#register-button').attr("disabled", true);
|
||||||
|
//$('#login-button').attr("disabled", true);
|
||||||
|
var el = document.getElementById("error-message");
|
||||||
|
el.textContent = "Your browser does not support FIDO2/WebAuthN";
|
||||||
|
el.classList.remove("d-none");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Get a form value
|
||||||
|
* @param {any} selector
|
||||||
|
*/
|
||||||
|
function value(selector) {
|
||||||
|
var el = document.querySelector(selector);
|
||||||
|
if (el.type === "checkbox") {
|
||||||
|
return el.checked;
|
||||||
|
}
|
||||||
|
return el.value;
|
||||||
|
}
|
||||||
66
BTCPayServer/wwwroot/js/webauthn/login.js
Normal file
66
BTCPayServer/wwwroot/js/webauthn/login.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
if (detectFIDOSupport() && makeAssertionOptions){
|
||||||
|
|
||||||
|
login(makeAssertionOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(makeAssertionOptions) {
|
||||||
|
|
||||||
|
console.log("Assertion Options Object", makeAssertionOptions);
|
||||||
|
const challenge = makeAssertionOptions.challenge.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
makeAssertionOptions.challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));
|
||||||
|
|
||||||
|
// fix escaping. Change this to coerce
|
||||||
|
makeAssertionOptions.allowCredentials.forEach(function (listItem) {
|
||||||
|
var fixedId = listItem.id.replace(/\_/g, "/").replace(/\-/g, "+");
|
||||||
|
listItem.id = Uint8Array.from(atob(fixedId), c => c.charCodeAt(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Assertion options", makeAssertionOptions);
|
||||||
|
|
||||||
|
|
||||||
|
let credential;
|
||||||
|
try {
|
||||||
|
credential = await navigator.credentials.get({ publicKey: makeAssertionOptions })
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err.message ? err.message : err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await verifyAssertionWithServer(credential);
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert("Could not verify assertion", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the credential to the the FIDO2 server for assertion
|
||||||
|
* @param {any} assertedCredential
|
||||||
|
*/
|
||||||
|
async function verifyAssertionWithServer(assertedCredential) {
|
||||||
|
|
||||||
|
// Move data into Arrays incase it is super long
|
||||||
|
let authData = new Uint8Array(assertedCredential.response.authenticatorData);
|
||||||
|
let clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
|
||||||
|
let rawId = new Uint8Array(assertedCredential.rawId);
|
||||||
|
let sig = new Uint8Array(assertedCredential.response.signature);
|
||||||
|
const data = {
|
||||||
|
id: assertedCredential.id,
|
||||||
|
rawId: coerceToBase64Url(rawId),
|
||||||
|
type: assertedCredential.type,
|
||||||
|
extensions: assertedCredential.getClientExtensionResults(),
|
||||||
|
response: {
|
||||||
|
authenticatorData: coerceToBase64Url(authData),
|
||||||
|
clientDataJson: coerceToBase64Url(clientDataJSON),
|
||||||
|
signature: coerceToBase64Url(sig)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById("Response").value = JSON.stringify(data);
|
||||||
|
document.getElementById("fidoForm").submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
67
BTCPayServer/wwwroot/js/webauthn/register.js
Normal file
67
BTCPayServer/wwwroot/js/webauthn/register.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
if (detectFIDOSupport() && makeCredentialOptions){
|
||||||
|
register(makeCredentialOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function register(makeCredentialOptions) {
|
||||||
|
console.log("Credential Options Object", makeCredentialOptions);
|
||||||
|
// Turn the challenge back into the accepted format of padded base64
|
||||||
|
makeCredentialOptions.challenge = coerceToArrayBuffer(makeCredentialOptions.challenge);
|
||||||
|
// Turn ID into a UInt8Array Buffer for some reason
|
||||||
|
makeCredentialOptions.user.id = coerceToArrayBuffer(makeCredentialOptions.user.id);
|
||||||
|
|
||||||
|
makeCredentialOptions.excludeCredentials = makeCredentialOptions.excludeCredentials.map((c) => {
|
||||||
|
c.id = coerceToArrayBuffer(c.id);
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (makeCredentialOptions.authenticatorSelection.authenticatorAttachment == null) makeCredentialOptions.authenticatorSelection.authenticatorAttachment = undefined;
|
||||||
|
|
||||||
|
console.log("Credential Options Formatted", makeCredentialOptions);
|
||||||
|
|
||||||
|
|
||||||
|
console.log("Creating PublicKeyCredential...");
|
||||||
|
|
||||||
|
let newCredential;
|
||||||
|
try {
|
||||||
|
newCredential = await navigator.credentials.create({
|
||||||
|
publicKey: makeCredentialOptions
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
var msg = "Could not create credentials in browser. Probably because the username is already registered with your authenticator. Please change username or authenticator."
|
||||||
|
showErrorAlert(msg, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("PublicKeyCredential Created", newCredential);
|
||||||
|
|
||||||
|
try {
|
||||||
|
registerNewCredential(newCredential);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(err.message ? err.message : err);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should be used to verify the auth data with the server
|
||||||
|
async function registerNewCredential(newCredential) {
|
||||||
|
// Move data into Arrays incase it is super long
|
||||||
|
let attestationObject = new Uint8Array(newCredential.response.attestationObject);
|
||||||
|
let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
|
||||||
|
let rawId = new Uint8Array(newCredential.rawId);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
id: newCredential.id,
|
||||||
|
rawId: coerceToBase64Url(rawId),
|
||||||
|
type: newCredential.type,
|
||||||
|
extensions: newCredential.getClientExtensionResults(),
|
||||||
|
response: {
|
||||||
|
AttestationObject: coerceToBase64Url(attestationObject),
|
||||||
|
clientDataJson: coerceToBase64Url(clientDataJSON)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("data").value = JSON.stringify(data);
|
||||||
|
document.getElementById("registerForm").submit();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user