From d5e033b5daea7d2c3abbb5e68f80967ba781dd7d Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Tue, 11 Mar 2025 17:41:33 +0330
Subject: [PATCH 01/20] fix WebAuthn issues in Boilerplate #10228

---
 .../Layout/AppDiagnosticModal.razor           |  6 ++++
 .../Layout/AppDiagnosticModal.razor.cs        |  9 +++++
 .../Settings/PasswordlessSection.razor        |  4 +--
 .../Authorized/Settings/SettingsPage.razor.cs |  5 ++-
 .../Scripts/WebAuthn.ts                       | 35 ++++++++++---------
 .../Identity/UserController.WebAuthn.cs       | 31 +++++++++++-----
 .../Models/Identity/WebAuthnCredential.cs     |  5 +++
 .../Controllers/Identity/IUserController.cs   |  3 ++
 8 files changed, 70 insertions(+), 28 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor
index fc7619c2be..1569c32697 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor
@@ -145,6 +145,12 @@
                            Variant="BitVariant.Text"
                            OnClick="() => isLogModalOpen = false"
                            IconName="@BitIconName.ChromeClose" />
+                <BitButton IconOnly
+                           AutoLoading
+                           Color="BitColor.Info"
+                           Variant="BitVariant.Text"
+                           IconName="@BitIconName.Delete"
+                           OnClick="DeleteAllWebAuthnCredentials" />
             </BitStack>
             <br />
             <BitText Class="log-modal" Color="GetColor(selectedLog?.Level)">
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs
index 38944c9096..3b19c69c9e 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs
@@ -3,6 +3,7 @@
 //#endif
 using Boilerplate.Shared.Controllers.Diagnostics;
 using Boilerplate.Client.Core.Services.DiagnosticLog;
+using Boilerplate.Shared.Controllers.Identity;
 
 namespace Boilerplate.Client.Core.Components.Layout;
 
@@ -41,6 +42,7 @@ public partial class AppDiagnosticModal
     //#if (notification == true)
     [AutoInject] private IPushNotificationService pushNotificationService = default!;
     //#endif
+    [AutoInject] private IUserController userController = default!;
 
     protected override Task OnInitAsync()
     {
@@ -154,6 +156,13 @@ private void ResetLogs()
         FilterLogs();
     }
 
+    private async Task DeleteAllWebAuthnCredentials()
+    {
+        if ((await AuthenticationStateTask).User.IsAuthenticated() is false) return;
+
+        await userController.DeleteAllWebAuthnCredentials(CurrentCancellationToken);
+    }
+
     private static BitColor GetColor(LogLevel? level)
     {
         return level switch
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/PasswordlessSection.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/PasswordlessSection.razor
index 918afd886a..d312eb4634 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/PasswordlessSection.razor
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/PasswordlessSection.razor
@@ -8,13 +8,13 @@
         <br />
         @if (isConfigured)
         {
-            <BitButton OnClick="WrapHandled(DisablePasswordless)" Variant="BitVariant.Outline" Color="BitColor.Warning">
+            <BitButton AutoLoading OnClick="WrapHandled(DisablePasswordless)" Variant="BitVariant.Outline" Color="BitColor.Warning">
                 @Localizer[nameof(AppStrings.DisablePasswordless)]
             </BitButton>
         }
         else
         {
-            <BitButton OnClick="WrapHandled(EnablePasswordless)">
+            <BitButton AutoLoading OnClick="WrapHandled(EnablePasswordless)">
                 @Localizer[nameof(AppStrings.EnablePasswordless)]
             </BitButton>
         }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor.cs
index 6fb471140a..74634ba4ba 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor.cs
@@ -34,7 +34,10 @@ protected override async Task OnInitAsync()
         try
         {
             user = (await PrerenderStateService.GetValue(() => HttpClient.GetFromJsonAsync("api/User/GetCurrentUser", JsonSerializerOptions.GetTypeInfo<UserDto>(), CurrentCancellationToken)))!;
-            showPasswordless = await JSRuntime.IsWebAuthnAvailable() && AppPlatform.IsBlazorHybrid is false;
+            if (InPrerenderSession is false)
+            {
+                showPasswordless = await JSRuntime.IsWebAuthnAvailable() && AppPlatform.IsBlazorHybrid is false;
+            }
         }
         finally
         {
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
index d3bb2b27db..db4166174b 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
@@ -23,12 +23,13 @@ class WebAuthn {
 
 
     public static async createCredential(options: PublicKeyCredentialCreationOptions) {
+        console.log(options)
         if (typeof options.challenge === 'string') {
-            options.challenge = WebAuthn.urlToArray(options.challenge);
+            options.challenge = WebAuthn.stringToBinary(options.challenge);
         }
 
         if (typeof options.user.id === 'string') {
-            options.user.id = WebAuthn.urlToArray(options.user.id);
+            options.user.id = WebAuthn.stringToBinary(options.user.id);
         }
 
         if (options.rp.id === null) {
@@ -38,20 +39,20 @@ class WebAuthn {
         for (let cred of options.excludeCredentials || []) {
             if (typeof cred.id !== 'string') continue;
 
-            cred.id = WebAuthn.urlToArray(cred.id);
+            cred.id = WebAuthn.stringToBinary(cred.id);
         }
 
         const credential = await navigator.credentials.create({ publicKey: options }) as PublicKeyCredential;
         const response = credential.response as AuthenticatorAttestationResponse;
 
         return {
-            id: WebAuthn.base64ToUrl(credential.id),
-            rawId: WebAuthn.arrayToUrl(credential.rawId),
+            id: WebAuthn.base64ToString(credential.id),
+            rawId: WebAuthn.binaryToString(credential.rawId),
             type: credential.type,
             clientExtensionResults: credential.getClientExtensionResults(),
             response: {
-                attestationObject: WebAuthn.arrayToUrl(response.attestationObject),
-                clientDataJSON: WebAuthn.arrayToUrl(response.clientDataJSON),
+                attestationObject: WebAuthn.binaryToString(response.attestationObject),
+                clientDataJSON: WebAuthn.binaryToString(response.clientDataJSON),
                 transports: response.getTransports ? response.getTransports() : []
             }
         };
@@ -59,14 +60,14 @@ class WebAuthn {
 
     public static async verifyCredential(options: PublicKeyCredentialRequestOptions) {
         if (typeof options.challenge === 'string') {
-            options.challenge = WebAuthn.urlToArray(options.challenge);
+            options.challenge = WebAuthn.stringToBinary(options.challenge);
         }
 
         if (options.allowCredentials) {
             for (let i = 0; i < options.allowCredentials.length; i++) {
                 const id = options.allowCredentials[i].id;
                 if (typeof id === 'string') {
-                    options.allowCredentials[i].id = WebAuthn.urlToArray(id);
+                    options.allowCredentials[i].id = WebAuthn.stringToBinary(id);
                 }
             }
         }
@@ -75,29 +76,29 @@ class WebAuthn {
 
         return {
             id: credential.id,
-            rawId: WebAuthn.arrayToUrl(credential.rawId),
+            rawId: WebAuthn.binaryToString(credential.rawId),
             type: credential.type,
             clientExtensionResults: credential.getClientExtensionResults(),
             response: {
-                authenticatorData: WebAuthn.arrayToUrl(response.authenticatorData),
-                clientDataJSON: WebAuthn.arrayToUrl(response.clientDataJSON),
-                userHandle: response.userHandle && response.userHandle.byteLength > 0 ? WebAuthn.arrayToUrl(response.userHandle) : undefined,
-                signature: WebAuthn.arrayToUrl(response.signature)
+                authenticatorData: WebAuthn.binaryToString(response.authenticatorData),
+                clientDataJSON: WebAuthn.binaryToString(response.clientDataJSON),
+                userHandle: response.userHandle && response.userHandle.byteLength > 0 ? WebAuthn.binaryToString(response.userHandle) : undefined,
+                signature: WebAuthn.binaryToString(response.signature)
             }
         }
     }
 
 
 
-    private static arrayToUrl(value: ArrayBuffer): string {
+    private static binaryToString(value: ArrayBuffer): string {
         return btoa(String.fromCharCode(...new Uint8Array(value))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
     }
 
-    private static urlToArray(value: string): Uint8Array {
+    private static stringToBinary(value: string): Uint8Array {
         return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
     }
 
-    private static base64ToUrl(value: string): string {
+    private static base64ToString(value: string): string {
         return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
     }
 }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
index 51230b855a..beda9be5ba 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
@@ -3,7 +3,6 @@
 using Microsoft.Extensions.Caching.Distributed;
 using Fido2NetLib;
 using Fido2NetLib.Objects;
-using Boilerplate.Shared.Dtos.Identity;
 using Boilerplate.Server.Api.Models.Identity;
 
 namespace Boilerplate.Server.Api.Controllers.Identity;
@@ -36,14 +35,14 @@ public async Task<CredentialCreateOptions> GetWebAuthnCredentialOptions(Cancella
         {
             User = fidoUser,
             ExcludeCredentials = [.. existingKeys],
-            AuthenticatorSelection = AuthenticatorSelection.Default,
+            AuthenticatorSelection = new() { AuthenticatorAttachment = AuthenticatorAttachment.Platform },
             AttestationPreference = AttestationConveyancePreference.None,
-            Extensions = new AuthenticationExtensionsClientInputs
-            {
-                CredProps = true,
-                Extensions = true,
-                UserVerificationMethod = true,
-            }
+            //Extensions = new AuthenticationExtensionsClientInputs
+            //{
+            //    CredProps = true,
+            //    Extensions = true,
+            //    UserVerificationMethod = true,
+            //}
         });
 
         var key = GetWebAuthnCacheKey(userId);
@@ -114,6 +113,22 @@ public async Task DeleteWebAuthnCredential(byte[] credentialId, CancellationToke
         await DbContext.SaveChangesAsync(cancellationToken);
     }
 
+    [HttpDelete]
+    public async Task DeleteAllWebAuthnCredentials(CancellationToken cancellationToken)
+    {
+        var userId = User.GetUserId();
+        var user = await userManager.FindByIdAsync(userId.ToString())
+                    ?? throw new ResourceNotFoundException();
+
+        var entities = await DbContext.WebAuthnCredential.Where(c => c.UserId == userId).ToListAsync(cancellationToken);
+
+        if (entities is null || entities.Count == 0) return;
+
+        DbContext.WebAuthnCredential.RemoveRange(entities);
+
+        await DbContext.SaveChangesAsync(cancellationToken);
+    }
+
     private static string GetWebAuthnCacheKey(Guid userId) => $"WebAuthn_Options_{userId}";
 
     private async Task<bool> IsCredentialIdUniqueToUser(IsCredentialIdUniqueToUserParams args, CancellationToken cancellationToken)
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/WebAuthnCredential.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/WebAuthnCredential.cs
index a44563d2cc..1846cea1a6 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/WebAuthnCredential.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/WebAuthnCredential.cs
@@ -2,6 +2,11 @@
 
 namespace Boilerplate.Server.Api.Models.Identity;
 
+/// <summary>
+/// This model is used by the Fido2 lib to store and retrieve the data of the browser credential api for `Web Authentication`.
+/// <br />
+/// More info: <see href="https://github.com/passwordless-lib/fido2-net-lib"/>
+/// </summary>
 public class WebAuthnCredential
 {
     public required byte[] Id { get; set; }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs
index 720a75d8b6..9ecca46c8b 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs
@@ -57,4 +57,7 @@ public interface IUserController : IAppController
 
     [HttpDelete]
     Task DeleteWebAuthnCredential(byte[] credentialId, CancellationToken cancellationToken);
+
+    [HttpDelete]
+    Task DeleteAllWebAuthnCredentials(CancellationToken cancellationToken);
 }

From c9a58dbc6cd90916602106e4977fc518ac567088 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Tue, 11 Mar 2025 17:54:12 +0330
Subject: [PATCH 02/20] fix diag modal

---
 .../Layout/AppDiagnosticModal.razor           | 12 +++++------
 .../Layout/AppDiagnosticModal.razor.Utils.cs  | 21 ++++++++++++++-----
 .../Layout/AppDiagnosticModal.razor.cs        | 10 +--------
 .../IJSRuntimeWebAuthnExtensions.cs           |  2 +-
 .../Scripts/WebAuthn.ts                       |  3 +--
 5 files changed, 25 insertions(+), 23 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor
index 1569c32697..43cd6d8504 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor
@@ -92,6 +92,12 @@
                            Title="Clear cache"
                            Color="BitColor.SecondaryBackground"
                            IconName="@BitIconName.Clear" />
+                <BitButton IconOnly AutoLoading
+                           Color="BitColor.Info"
+                           Variant="BitVariant.Text"
+                           IconName="@BitIconName.Delete"
+                           Title="Clear WebAuthn"
+                           OnClick="ClearWebAuthn" />
             </BitStack>
 
             <BitBasicList @ref="logStackRef"
@@ -145,12 +151,6 @@
                            Variant="BitVariant.Text"
                            OnClick="() => isLogModalOpen = false"
                            IconName="@BitIconName.ChromeClose" />
-                <BitButton IconOnly
-                           AutoLoading
-                           Color="BitColor.Info"
-                           Variant="BitVariant.Text"
-                           IconName="@BitIconName.Delete"
-                           OnClick="DeleteAllWebAuthnCredentials" />
             </BitStack>
             <br />
             <BitText Class="log-modal" Color="GetColor(selectedLog?.Level)">
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
index 3e024b68b9..384590bfe3 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
@@ -1,6 +1,7 @@
 using System.Text;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
+using Boilerplate.Shared.Controllers.Identity;
 //#if (signalR == true)
 using Microsoft.AspNetCore.SignalR.Client;
 //#endif
@@ -9,9 +10,10 @@ namespace Boilerplate.Client.Core.Components.Layout;
 
 public partial class AppDiagnosticModal
 {
-    [AutoInject] Cookie cookie = default!;
-    [AutoInject] AuthManager authManager = default!;
-    [AutoInject] IStorageService storageService = default!;
+    [AutoInject] private Cookie cookie = default!;
+    [AutoInject] private AuthManager authManager = default!;
+    [AutoInject] private IStorageService storageService = default!;
+    [AutoInject] private IUserController userController = default!;
 
     private static async Task ThrowTestException()
     {
@@ -97,13 +99,13 @@ await Task.Run(() =>
         SnackBarService.Show("Memory After GC", GetMemoryUsage());
     }
 
-    string GetMemoryUsage()
+    private string GetMemoryUsage()
     {
         long memory = Environment.WorkingSet;
         return $"{memory / (1024.0 * 1024.0):F2} MB";
     }
 
-    async Task ClearCache()
+    private async Task ClearCache()
     {
         try
         {
@@ -127,4 +129,13 @@ async Task ClearCache()
             NavigationManager.Refresh(forceReload: true);
         }
     }
+
+    private async Task ClearWebAuthn()
+    {
+        if ((await AuthenticationStateTask).User.IsAuthenticated() is false) return;
+
+        await userController.DeleteAllWebAuthnCredentials(CurrentCancellationToken);
+
+        await JSRuntime.RemoveWebAuthnConfigured();
+    }
 }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs
index 3b19c69c9e..d87d928e20 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs
@@ -1,9 +1,9 @@
 //#if (signalR == true)
 using Microsoft.AspNetCore.SignalR.Client;
 //#endif
+using Boilerplate.Shared.Controllers.Identity;
 using Boilerplate.Shared.Controllers.Diagnostics;
 using Boilerplate.Client.Core.Services.DiagnosticLog;
-using Boilerplate.Shared.Controllers.Identity;
 
 namespace Boilerplate.Client.Core.Components.Layout;
 
@@ -42,7 +42,6 @@ public partial class AppDiagnosticModal
     //#if (notification == true)
     [AutoInject] private IPushNotificationService pushNotificationService = default!;
     //#endif
-    [AutoInject] private IUserController userController = default!;
 
     protected override Task OnInitAsync()
     {
@@ -156,13 +155,6 @@ private void ResetLogs()
         FilterLogs();
     }
 
-    private async Task DeleteAllWebAuthnCredentials()
-    {
-        if ((await AuthenticationStateTask).User.IsAuthenticated() is false) return;
-
-        await userController.DeleteAllWebAuthnCredentials(CurrentCancellationToken);
-    }
-
     private static BitColor GetColor(LogLevel? level)
     {
         return level switch
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeWebAuthnExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeWebAuthnExtensions.cs
index 49be599d9f..18cda6f79f 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeWebAuthnExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeWebAuthnExtensions.cs
@@ -20,7 +20,7 @@ public static ValueTask<bool> IsWebAuthnConfigured(this IJSRuntime jsRuntime, st
         return jsRuntime.InvokeAsync<bool>("WebAuthn.isConfigured", username);
     }
 
-    public static ValueTask RemoveWebAuthnConfigured(this IJSRuntime jsRuntime, string username)
+    public static ValueTask RemoveWebAuthnConfigured(this IJSRuntime jsRuntime, string? username = null)
     {
         return jsRuntime.InvokeVoidAsync("WebAuthn.removeConfigured", username);
     }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
index db4166174b..fbd08dfb62 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
@@ -18,12 +18,11 @@ class WebAuthn {
 
     public static removeConfigured(username: string) {
         const storedCredentials = JSON.parse(localStorage.getItem(WebAuthn.STORE_KEY) || '[]') as string[];
-        localStorage.setItem(WebAuthn.STORE_KEY, JSON.stringify(storedCredentials.filter(c => c !== username)));
+        localStorage.setItem(WebAuthn.STORE_KEY, JSON.stringify(!!username ? storedCredentials.filter(c => c !== username) : []));
     }
 
 
     public static async createCredential(options: PublicKeyCredentialCreationOptions) {
-        console.log(options)
         if (typeof options.challenge === 'string') {
             options.challenge = WebAuthn.stringToBinary(options.challenge);
         }

From 33d86efb652bc47e38ddb820d9eaf286d7a0caa1 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Tue, 11 Mar 2025 17:58:56 +0330
Subject: [PATCH 03/20] add missing unknown.png

---
 .../wwwroot/images/os/unknown.png                 | Bin 0 -> 909 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/wwwroot/images/os/unknown.png

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/wwwroot/images/os/unknown.png b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/wwwroot/images/os/unknown.png
new file mode 100644
index 0000000000000000000000000000000000000000..a440991e8f089ceabd90b0d486f151c1c9192ec0
GIT binary patch
literal 909
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}Ea{HEjtmSN
z`?>!lvI6;>1s;*b3=DcoAk4Vx(cMWvL5ULAh?3y^w370~qEv=}#LT=BJwMkF1yemk
zJrkk(%bI~|wxvdRrg?g5F>nAmtPE0&tPG4mmKP99L)jqLXfQH^#hHL?Lq;YB0U#X(
z#F_0ZVDT&<8wBo8W?^{2jG)mhWdKS|U}s<fsx&Y%Heg%;F%@JZ>jH>LQ-Ev`U;>)U
z1XdYjX#r$Gbr~8MfMnOmdVbt7+gc2$&c@TlF(ktM>~!xFevT3i$77QeH!4h0DZ1H_
z@^IgViLIM92f27@N^?u!ys@&r`OTUX)su5Qk`k2zgTlm4H7RZXVDo&=#qY)Y-k+H{
zxqtb*dcD$fd(zYP&HT>)f0kkBP8Rb=+ZTQPF4@2RW8}rAzdw>sSfxq4mY29T{d-W4
z<n#pod0ML<irqWX>AG5O{{!th{V2sA=jEpR*7xb}kTe%vEAZ{n%S};i0?VImkmWpc
zeCrOb^cxc!-uFGWc(z{kMXFQiw`9qC2gNJo<sWMAY2Ypts6Uex%g{WdcY2|%)l>No
z#(qa-UtBtSFy$6ofxz==R}0w09|rBoVn1`ZHr2Fu8{-k)FLM14vTHhz8*bX8H914L
z!TOIt-tpWYV#=3S1>Flx70P8Qcz*A2cg6Os(@ZvjVch4!fnw7ilwW(uTq1uWa8F3s
z<rhK+N^IIUUx+`fXVbP8$n)yi&$0hbLeyndhW>}ge+aC<J<)kO!ye-~t)@SY^vUdE
zd61bQU|z6Q*0O~2z*qCmk8Phj&Moq0$p4^e^>=T3koGI45A*yVN>v%JbKqYga)#3_
zaQcK9t1~Ze)%j>5Go{1jkHK`GrTI=qI_|0}_ud{}CSmM%H|b8p%9AUzcS`T&K63h{
z4&N!|>cWD}tDd`RPo6BX{Bo6;@9cD;yrU;yI3&H~kvX7S*3f>D!;1Oi+-dVaaLwA5
zYAAfR?$PvZSJP|V<AeAXy?L?l<IO1ybN<Ch&Ck8Xye~oB@8FO40Ij+^?k29q`hWUc
aJ_zp%Tah`}uI3^rO?kTdxvX<aXaWHJmR70&

literal 0
HcmV?d00001


From 389c4a38a9a8fe096a2dfd50fff1e82b1387ac90 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Tue, 11 Mar 2025 18:07:11 +0330
Subject: [PATCH 04/20] remove more options props

---
 .../Controllers/Identity/IdentityController.WebAuthn.cs       | 4 ++--
 .../Controllers/Identity/UserController.WebAuthn.cs           | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
index ba63186b45..0bebe8f350 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
@@ -28,8 +28,8 @@ public async Task<AssertionOptions> GetWebAuthnAssertionOptions(CancellationToke
 
         var options = fido2.GetAssertionOptions(new GetAssertionOptionsParams
         {
-            Extensions = extensions,
-            AllowedCredentials = existingKeys,
+            //Extensions = extensions,
+            //AllowedCredentials = existingKeys,
             UserVerification = UserVerificationRequirement.Discouraged,
         });
 
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
index beda9be5ba..365faa34be 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
@@ -34,9 +34,9 @@ public async Task<CredentialCreateOptions> GetWebAuthnCredentialOptions(Cancella
         var options = fido2.RequestNewCredential(new RequestNewCredentialParams
         {
             User = fidoUser,
-            ExcludeCredentials = [.. existingKeys],
+            //ExcludeCredentials = [.. existingKeys],
             AuthenticatorSelection = new() { AuthenticatorAttachment = AuthenticatorAttachment.Platform },
-            AttestationPreference = AttestationConveyancePreference.None,
+            //AttestationPreference = AttestationConveyancePreference.None,
             //Extensions = new AuthenticationExtensionsClientInputs
             //{
             //    CredProps = true,

From 5e7b2b596e9817f53051d994995e382f2da88799 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Tue, 11 Mar 2025 19:27:05 +0330
Subject: [PATCH 05/20] add logs

---
 .../Scripts/WebAuthn.ts                        | 14 ++++++++++----
 .../Identity/IdentityController.WebAuthn.cs    |  6 +++---
 .../Identity/UserController.WebAuthn.cs        | 18 +++++++++---------
 3 files changed, 22 insertions(+), 16 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
index fbd08dfb62..85ecf94ed4 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
@@ -23,6 +23,7 @@ class WebAuthn {
 
 
     public static async createCredential(options: PublicKeyCredentialCreationOptions) {
+        console.log(options)
         if (typeof options.challenge === 'string') {
             options.challenge = WebAuthn.stringToBinary(options.challenge);
         }
@@ -43,8 +44,8 @@ class WebAuthn {
 
         const credential = await navigator.credentials.create({ publicKey: options }) as PublicKeyCredential;
         const response = credential.response as AuthenticatorAttestationResponse;
-
-        return {
+        console.log('response:', response)
+        const result = {
             id: WebAuthn.base64ToString(credential.id),
             rawId: WebAuthn.binaryToString(credential.rawId),
             type: credential.type,
@@ -55,9 +56,12 @@ class WebAuthn {
                 transports: response.getTransports ? response.getTransports() : []
             }
         };
+        console.log('result:', result)
+        return result;
     }
 
     public static async verifyCredential(options: PublicKeyCredentialRequestOptions) {
+        console.log(options)
         if (typeof options.challenge === 'string') {
             options.challenge = WebAuthn.stringToBinary(options.challenge);
         }
@@ -72,8 +76,8 @@ class WebAuthn {
         }
         const credential = await navigator.credentials.get({ publicKey: options }) as PublicKeyCredential;
         const response = credential.response as AuthenticatorAssertionResponse;
-
-        return {
+        console.log('response:', response)
+        var result = {
             id: credential.id,
             rawId: WebAuthn.binaryToString(credential.rawId),
             type: credential.type,
@@ -85,6 +89,8 @@ class WebAuthn {
                 signature: WebAuthn.binaryToString(response.signature)
             }
         }
+        console.log('result:', result)
+        return result;
     }
 
 
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
index 0bebe8f350..f677138a13 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
@@ -22,14 +22,14 @@ public async Task<AssertionOptions> GetWebAuthnAssertionOptions(CancellationToke
 
         var extensions = new AuthenticationExtensionsClientInputs
         {
+            Extensions = true,
             UserVerificationMethod = true,
-            Extensions = true
         };
 
         var options = fido2.GetAssertionOptions(new GetAssertionOptionsParams
         {
-            //Extensions = extensions,
-            //AllowedCredentials = existingKeys,
+            Extensions = extensions,
+            AllowedCredentials = existingKeys,
             UserVerification = UserVerificationRequirement.Discouraged,
         });
 
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
index 365faa34be..2fac870fe1 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
@@ -34,15 +34,15 @@ public async Task<CredentialCreateOptions> GetWebAuthnCredentialOptions(Cancella
         var options = fido2.RequestNewCredential(new RequestNewCredentialParams
         {
             User = fidoUser,
-            //ExcludeCredentials = [.. existingKeys],
-            AuthenticatorSelection = new() { AuthenticatorAttachment = AuthenticatorAttachment.Platform },
-            //AttestationPreference = AttestationConveyancePreference.None,
-            //Extensions = new AuthenticationExtensionsClientInputs
-            //{
-            //    CredProps = true,
-            //    Extensions = true,
-            //    UserVerificationMethod = true,
-            //}
+            ExcludeCredentials = [.. existingKeys],
+            AuthenticatorSelection = AuthenticatorSelection.Default, // new() { AuthenticatorAttachment = AuthenticatorAttachment.Platform },
+            AttestationPreference = AttestationConveyancePreference.None,
+            Extensions = new AuthenticationExtensionsClientInputs
+            {
+                CredProps = true,
+                Extensions = true,
+                UserVerificationMethod = true,
+            }
         });
 
         var key = GetWebAuthnCacheKey(userId);

From b1a93f84e8e5d1dc630bbffed741cf8b8b504eb7 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Wed, 12 Mar 2025 10:37:34 +0330
Subject: [PATCH 06/20] fix ts

---
 .../Scripts/WebAuthn.ts                       | 195 ++++++++++++++----
 .../Identity/UserController.WebAuthn.cs       |  22 +-
 2 files changed, 166 insertions(+), 51 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
index 85ecf94ed4..03b3db6307 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
@@ -23,89 +23,196 @@ class WebAuthn {
 
 
     public static async createCredential(options: PublicKeyCredentialCreationOptions) {
-        console.log(options)
-        if (typeof options.challenge === 'string') {
-            options.challenge = WebAuthn.stringToBinary(options.challenge);
+        console.log(options);
+
+        //if (typeof options.challenge === 'string') {
+        //    options.challenge = WebAuthn.stringToBinary(options.challenge);
+        //}
+
+        options.challenge = WebAuthn.ToArrayBuffer(options.challenge, 'challenge');
+
+        //if (typeof options.user.id === 'string') {
+        //    options.user.id = WebAuthn.stringToBinary(options.user.id);
+        //}
+
+        options.user.id = WebAuthn.ToArrayBuffer(options.user.id, 'user.id');
+
+        for (let cred of options.excludeCredentials || []) {
+            //if (typeof cred.id !== 'string') continue;
+            //cred.id = WebAuthn.stringToBinary(cred.id);
+            cred.id = WebAuthn.ToArrayBuffer(cred.id, 'cred.id');
         }
 
-        if (typeof options.user.id === 'string') {
-            options.user.id = WebAuthn.stringToBinary(options.user.id);
+        if (options.authenticatorSelection?.authenticatorAttachment === null) {
+            options.authenticatorSelection.authenticatorAttachment = undefined;
         }
 
         if (options.rp.id === null) {
             options.rp.id = undefined;
         }
 
-        for (let cred of options.excludeCredentials || []) {
-            if (typeof cred.id !== 'string') continue;
-
-            cred.id = WebAuthn.stringToBinary(cred.id);
-        }
+        console.log('corrected:', options);
 
         const credential = await navigator.credentials.create({ publicKey: options }) as PublicKeyCredential;
+
+        console.log('credential:', credential);
+
         const response = credential.response as AuthenticatorAttestationResponse;
-        console.log('response:', response)
+
+        // Move data into Arrays incase it is super long
+        const attestationObject = new Uint8Array(response.attestationObject);
+        const clientDataJSON = new Uint8Array(response.clientDataJSON);
+        const rawId = new Uint8Array(credential.rawId);
+
         const result = {
-            id: WebAuthn.base64ToString(credential.id),
-            rawId: WebAuthn.binaryToString(credential.rawId),
+            id: credential.id,
+            rawId: WebAuthn.ToBase64Url(rawId),
             type: credential.type,
             clientExtensionResults: credential.getClientExtensionResults(),
             response: {
-                attestationObject: WebAuthn.binaryToString(response.attestationObject),
-                clientDataJSON: WebAuthn.binaryToString(response.clientDataJSON),
+                attestationObject: WebAuthn.ToBase64Url(attestationObject),
+                clientDataJSON: WebAuthn.ToBase64Url(clientDataJSON),
                 transports: response.getTransports ? response.getTransports() : []
             }
         };
-        console.log('result:', result)
+
+        console.log('result:', result);
+
         return result;
     }
 
     public static async verifyCredential(options: PublicKeyCredentialRequestOptions) {
-        console.log(options)
-        if (typeof options.challenge === 'string') {
-            options.challenge = WebAuthn.stringToBinary(options.challenge);
-        }
+        console.log(options);
+
+        //if (typeof options.challenge === 'string') {
+        //    options.challenge = WebAuthn.stringToBinary(options.challenge);
+        //}
+
+        options.challenge = WebAuthn.ToArrayBuffer(options.challenge, 'challenge');
+
+        //if (options.allowCredentials) {
+        //    for (let i = 0; i < options.allowCredentials.length; i++) {
+        //        const id = options.allowCredentials[i].id;
+        //        if (typeof id === 'string') {
+        //            options.allowCredentials[i].id = WebAuthn.stringToBinary(id);
+        //        }
+        //    }
+        //}
+
+        options.allowCredentials?.forEach(function (cred) {
+            cred.id = WebAuthn.ToArrayBuffer(cred.id, 'cred.id');
+        });
+
+        console.log('corrected:', options);
 
-        if (options.allowCredentials) {
-            for (let i = 0; i < options.allowCredentials.length; i++) {
-                const id = options.allowCredentials[i].id;
-                if (typeof id === 'string') {
-                    options.allowCredentials[i].id = WebAuthn.stringToBinary(id);
-                }
-            }
-        }
         const credential = await navigator.credentials.get({ publicKey: options }) as PublicKeyCredential;
+
+        console.log('credential:', credential);
+
         const response = credential.response as AuthenticatorAssertionResponse;
-        console.log('response:', response)
+
+        // Move data into Arrays incase it is super long
+        let authenticatorData = new Uint8Array(response.authenticatorData);
+        let clientDataJSON = new Uint8Array(response.clientDataJSON);
+        let rawId = new Uint8Array(credential.rawId);
+        let signature = new Uint8Array(response.signature);
+        let userHandle = new Uint8Array(response.userHandle || []);
+
         var result = {
             id: credential.id,
-            rawId: WebAuthn.binaryToString(credential.rawId),
+            rawId: WebAuthn.ToBase64Url(rawId),
             type: credential.type,
             clientExtensionResults: credential.getClientExtensionResults(),
             response: {
-                authenticatorData: WebAuthn.binaryToString(response.authenticatorData),
-                clientDataJSON: WebAuthn.binaryToString(response.clientDataJSON),
-                userHandle: response.userHandle && response.userHandle.byteLength > 0 ? WebAuthn.binaryToString(response.userHandle) : undefined,
-                signature: WebAuthn.binaryToString(response.signature)
+                authenticatorData: WebAuthn.ToBase64Url(authenticatorData),
+                clientDataJSON: WebAuthn.ToBase64Url(clientDataJSON),
+                userHandle: userHandle !== null ? WebAuthn.ToBase64Url(userHandle) : null,
+                signature: WebAuthn.ToBase64Url(signature)
             }
         }
-        console.log('result:', result)
+
+        console.log('result:', result);
+
         return result;
     }
 
 
 
-    private static binaryToString(value: ArrayBuffer): string {
-        return btoa(String.fromCharCode(...new Uint8Array(value))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
-    }
+    //private static stringToBinary(value: string): Uint8Array {
+    //    return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
+    //}
 
-    private static stringToBinary(value: string): Uint8Array {
-        return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
-    }
+    //private static binaryToString(value: ArrayBuffer): string {
+    //    return btoa(String.fromCharCode(...new Uint8Array(value))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
+    //}
 
-    private static base64ToString(value: string): string {
-        return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
-    }
+    //private static base64ToString(value: string): string {
+    //    return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
+    //}
+
+    private static ToArrayBuffer(value: any, name: string) {
+        if (typeof value === "string") {
+            // base64url to base64
+            value = value.replace(/-/g, "+").replace(/_/g, "/");
+
+            // base64 to Uint8Array
+            var str = window.atob(value);
+            var bytes = new Uint8Array(str.length);
+            for (var i = 0; i < str.length; i++) {
+                bytes[i] = str.charCodeAt(i);
+            }
+            value = bytes;
+        }
+
+        // Array to Uint8Array
+        if (Array.isArray(value)) {
+            value = new Uint8Array(value);
+        }
+
+        // Uint8Array to ArrayBuffer
+        if (value instanceof Uint8Array) {
+            value = value.buffer;
+        }
+
+        // error if none of the above worked
+        if (!(value instanceof ArrayBuffer)) {
+            throw new TypeError("could not coerce '" + name + "' to ArrayBuffer");
+        }
+
+        return value;
+    };
+
+    private static ToBase64Url(value: any) {
+        // Array or ArrayBuffer to Uint8Array
+        if (Array.isArray(value)) {
+            value = Uint8Array.from(value);
+        }
+
+        if (value instanceof ArrayBuffer) {
+            value = new Uint8Array(value);
+        }
+
+        // Uint8Array to base64
+        if (value instanceof Uint8Array) {
+            var str = "";
+            var len = value.byteLength;
+
+            for (var i = 0; i < len; i++) {
+                str += String.fromCharCode(value[i]);
+            }
+            value = window.btoa(str);
+        }
+
+        if (typeof value !== "string") {
+            throw new Error("could not coerce to string");
+        }
+
+        // base64 to base64url
+        // NOTE: "=" at the end of challenge is optional, strip it off here
+        value = value.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
+
+        return value;
+    };
 }
 
 (window as any).WebAuthn = WebAuthn;
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
index 2fac870fe1..7fb0c7f0a6 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
@@ -31,18 +31,26 @@ public async Task<CredentialCreateOptions> GetWebAuthnCredentialOptions(Cancella
             DisplayName = user.DisplayName,
         };
 
+        var authenticatorSelection = new AuthenticatorSelection
+        {
+            ResidentKey = ResidentKeyRequirement.Required,
+            UserVerification = UserVerificationRequirement.Preferred
+        };
+
+        var extensions = new AuthenticationExtensionsClientInputs
+        {
+            CredProps = true,
+            Extensions = true,
+            UserVerificationMethod = true,
+        };
+
         var options = fido2.RequestNewCredential(new RequestNewCredentialParams
         {
             User = fidoUser,
             ExcludeCredentials = [.. existingKeys],
-            AuthenticatorSelection = AuthenticatorSelection.Default, // new() { AuthenticatorAttachment = AuthenticatorAttachment.Platform },
+            AuthenticatorSelection = authenticatorSelection,
             AttestationPreference = AttestationConveyancePreference.None,
-            Extensions = new AuthenticationExtensionsClientInputs
-            {
-                CredProps = true,
-                Extensions = true,
-                UserVerificationMethod = true,
-            }
+            Extensions = extensions
         });
 
         var key = GetWebAuthnCacheKey(userId);

From fac1b4cfeedea4d0330eaff900ecebede5bcd553 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Wed, 12 Mar 2025 11:17:02 +0330
Subject: [PATCH 07/20] remove redundant stuff

---
 .../Layout/AppDiagnosticModal.razor.cs        |  1 -
 .../Scripts/WebAuthn.ts                       | 80 ++++++++-----------
 2 files changed, 34 insertions(+), 47 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs
index d87d928e20..38944c9096 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.cs
@@ -1,7 +1,6 @@
 //#if (signalR == true)
 using Microsoft.AspNetCore.SignalR.Client;
 //#endif
-using Boilerplate.Shared.Controllers.Identity;
 using Boilerplate.Shared.Controllers.Diagnostics;
 using Boilerplate.Client.Core.Services.DiagnosticLog;
 
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
index 03b3db6307..59dd2a72dc 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
@@ -1,5 +1,5 @@
 class WebAuthn {
-    private static STORE_KEY = 'configured-webauthn';
+    private static STORE_KEY = 'webauthn';
 
     public static isAvailable() {
         return !!window.PublicKeyCredential;
@@ -11,14 +11,14 @@ class WebAuthn {
         localStorage.setItem(WebAuthn.STORE_KEY, JSON.stringify(storedCredentials));
     }
 
-    public static isConfigured(username: string | undefined) {
+    public static removeConfigured(username: string) {
         const storedCredentials = JSON.parse(localStorage.getItem(WebAuthn.STORE_KEY) || '[]') as string[];
-        return !!username ? storedCredentials.includes(username) : storedCredentials.length > 0;
+        localStorage.setItem(WebAuthn.STORE_KEY, JSON.stringify(!!username ? storedCredentials.filter(c => c !== username) : []));
     }
 
-    public static removeConfigured(username: string) {
+    public static isConfigured(username: string | undefined) {
         const storedCredentials = JSON.parse(localStorage.getItem(WebAuthn.STORE_KEY) || '[]') as string[];
-        localStorage.setItem(WebAuthn.STORE_KEY, JSON.stringify(!!username ? storedCredentials.filter(c => c !== username) : []));
+        return !!username ? storedCredentials.includes(username) : storedCredentials.length > 0;
     }
 
 
@@ -138,17 +138,37 @@ class WebAuthn {
 
 
 
-    //private static stringToBinary(value: string): Uint8Array {
-    //    return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
-    //}
+    private static ToBase64Url(value: any) {
+        // Array or ArrayBuffer to Uint8Array
+        if (Array.isArray(value)) {
+            value = Uint8Array.from(value);
+        }
 
-    //private static binaryToString(value: ArrayBuffer): string {
-    //    return btoa(String.fromCharCode(...new Uint8Array(value))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
-    //}
+        if (value instanceof ArrayBuffer) {
+            value = new Uint8Array(value);
+        }
 
-    //private static base64ToString(value: string): string {
-    //    return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
-    //}
+        // Uint8Array to base64
+        if (value instanceof Uint8Array) {
+            var str = "";
+            var len = value.byteLength;
+
+            for (var i = 0; i < len; i++) {
+                str += String.fromCharCode(value[i]);
+            }
+            value = window.btoa(str);
+        }
+
+        if (typeof value !== "string") {
+            throw new Error("could not coerce to string");
+        }
+
+        // base64 to base64url
+        // NOTE: "=" at the end of challenge is optional, strip it off here
+        value = value.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
+
+        return value;
+    };
 
     private static ToArrayBuffer(value: any, name: string) {
         if (typeof value === "string") {
@@ -181,38 +201,6 @@ class WebAuthn {
 
         return value;
     };
-
-    private static ToBase64Url(value: any) {
-        // Array or ArrayBuffer to Uint8Array
-        if (Array.isArray(value)) {
-            value = Uint8Array.from(value);
-        }
-
-        if (value instanceof ArrayBuffer) {
-            value = new Uint8Array(value);
-        }
-
-        // Uint8Array to base64
-        if (value instanceof Uint8Array) {
-            var str = "";
-            var len = value.byteLength;
-
-            for (var i = 0; i < len; i++) {
-                str += String.fromCharCode(value[i]);
-            }
-            value = window.btoa(str);
-        }
-
-        if (typeof value !== "string") {
-            throw new Error("could not coerce to string");
-        }
-
-        // base64 to base64url
-        // NOTE: "=" at the end of challenge is optional, strip it off here
-        value = value.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
-
-        return value;
-    };
 }
 
 (window as any).WebAuthn = WebAuthn;

From 25d2f0f5f2bdc7ba0fdf918b0a7d2b5051efee85 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Wed, 12 Mar 2025 11:45:07 +0330
Subject: [PATCH 08/20] remove redundant codes

---
 .../Scripts/WebAuthn.ts                       | 49 ++-----------------
 1 file changed, 5 insertions(+), 44 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
index 59dd2a72dc..60144ae5b1 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
@@ -5,6 +5,11 @@ class WebAuthn {
         return !!window.PublicKeyCredential;
     }
 
+    public static isConfigured(username: string | undefined) {
+        const storedCredentials = JSON.parse(localStorage.getItem(WebAuthn.STORE_KEY) || '[]') as string[];
+        return !!username ? storedCredentials.includes(username) : storedCredentials.length > 0;
+    }
+
     public static storeConfigured(username: string) {
         const storedCredentials = JSON.parse(localStorage.getItem(WebAuthn.STORE_KEY) || '[]') as string[];
         storedCredentials.push(username);
@@ -16,30 +21,13 @@ class WebAuthn {
         localStorage.setItem(WebAuthn.STORE_KEY, JSON.stringify(!!username ? storedCredentials.filter(c => c !== username) : []));
     }
 
-    public static isConfigured(username: string | undefined) {
-        const storedCredentials = JSON.parse(localStorage.getItem(WebAuthn.STORE_KEY) || '[]') as string[];
-        return !!username ? storedCredentials.includes(username) : storedCredentials.length > 0;
-    }
-
 
     public static async createCredential(options: PublicKeyCredentialCreationOptions) {
-        console.log(options);
-
-        //if (typeof options.challenge === 'string') {
-        //    options.challenge = WebAuthn.stringToBinary(options.challenge);
-        //}
-
         options.challenge = WebAuthn.ToArrayBuffer(options.challenge, 'challenge');
 
-        //if (typeof options.user.id === 'string') {
-        //    options.user.id = WebAuthn.stringToBinary(options.user.id);
-        //}
-
         options.user.id = WebAuthn.ToArrayBuffer(options.user.id, 'user.id');
 
         for (let cred of options.excludeCredentials || []) {
-            //if (typeof cred.id !== 'string') continue;
-            //cred.id = WebAuthn.stringToBinary(cred.id);
             cred.id = WebAuthn.ToArrayBuffer(cred.id, 'cred.id');
         }
 
@@ -51,12 +39,8 @@ class WebAuthn {
             options.rp.id = undefined;
         }
 
-        console.log('corrected:', options);
-
         const credential = await navigator.credentials.create({ publicKey: options }) as PublicKeyCredential;
 
-        console.log('credential:', credential);
-
         const response = credential.response as AuthenticatorAttestationResponse;
 
         // Move data into Arrays incase it is super long
@@ -76,39 +60,18 @@ class WebAuthn {
             }
         };
 
-        console.log('result:', result);
-
         return result;
     }
 
     public static async verifyCredential(options: PublicKeyCredentialRequestOptions) {
-        console.log(options);
-
-        //if (typeof options.challenge === 'string') {
-        //    options.challenge = WebAuthn.stringToBinary(options.challenge);
-        //}
-
         options.challenge = WebAuthn.ToArrayBuffer(options.challenge, 'challenge');
 
-        //if (options.allowCredentials) {
-        //    for (let i = 0; i < options.allowCredentials.length; i++) {
-        //        const id = options.allowCredentials[i].id;
-        //        if (typeof id === 'string') {
-        //            options.allowCredentials[i].id = WebAuthn.stringToBinary(id);
-        //        }
-        //    }
-        //}
-
         options.allowCredentials?.forEach(function (cred) {
             cred.id = WebAuthn.ToArrayBuffer(cred.id, 'cred.id');
         });
 
-        console.log('corrected:', options);
-
         const credential = await navigator.credentials.get({ publicKey: options }) as PublicKeyCredential;
 
-        console.log('credential:', credential);
-
         const response = credential.response as AuthenticatorAssertionResponse;
 
         // Move data into Arrays incase it is super long
@@ -131,8 +94,6 @@ class WebAuthn {
             }
         }
 
-        console.log('result:', result);
-
         return result;
     }
 

From 207c10c5e8d5b7edeef7d17e0bfd00bb00f87291 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Wed, 12 Mar 2025 11:46:53 +0330
Subject: [PATCH 09/20] rename store key

---
 .../src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
index 60144ae5b1..ab1b60354b 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
@@ -1,5 +1,5 @@
 class WebAuthn {
-    private static STORE_KEY = 'webauthn';
+    private static STORE_KEY = 'bit-webauthn';
 
     public static isAvailable() {
         return !!window.PublicKeyCredential;

From 5a6d191fb85bf34916b037aa3a1c712626385c72 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Wed, 12 Mar 2025 11:50:32 +0330
Subject: [PATCH 10/20] merge clear webauth button in diag modal

---
 .../Components/Layout/AppDiagnosticModal.razor | 10 ++--------
 .../Layout/AppDiagnosticModal.razor.Utils.cs   | 18 ++++++++----------
 2 files changed, 10 insertions(+), 18 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor
index 43cd6d8504..d8c9db1bb6 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor
@@ -88,16 +88,10 @@
                                IconName="@BitIconName.RecycleBin" />
                 }
                 <BitButton IconOnly AutoLoading
-                           OnClick="ClearCache"
-                           Title="Clear cache"
+                           OnClick="ClearData"
+                           Title="Clear data"
                            Color="BitColor.SecondaryBackground"
                            IconName="@BitIconName.Clear" />
-                <BitButton IconOnly AutoLoading
-                           Color="BitColor.Info"
-                           Variant="BitVariant.Text"
-                           IconName="@BitIconName.Delete"
-                           Title="Clear WebAuthn"
-                           OnClick="ClearWebAuthn" />
             </BitStack>
 
             <BitBasicList @ref="logStackRef"
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
index 384590bfe3..cc7d9e607e 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
@@ -105,7 +105,7 @@ private string GetMemoryUsage()
         return $"{memory / (1024.0 * 1024.0):F2} MB";
     }
 
-    private async Task ClearCache()
+    private async Task ClearData()
     {
         try
         {
@@ -120,6 +120,13 @@ private async Task ClearCache()
             await cookie.Remove(item.Name!);
         }
 
+        if ((await AuthenticationStateTask).User.IsAuthenticated())
+        {
+            await userController.DeleteAllWebAuthnCredentials(CurrentCancellationToken);
+
+            await JSRuntime.RemoveWebAuthnConfigured();
+        }
+
         if (AppPlatform.IsBlazorHybrid is false)
         {
             await JSRuntime.InvokeVoidAsync("BitBswup.forceRefresh"); // Clears cache storages and uninstalls service-worker.
@@ -129,13 +136,4 @@ private async Task ClearCache()
             NavigationManager.Refresh(forceReload: true);
         }
     }
-
-    private async Task ClearWebAuthn()
-    {
-        if ((await AuthenticationStateTask).User.IsAuthenticated() is false) return;
-
-        await userController.DeleteAllWebAuthnCredentials(CurrentCancellationToken);
-
-        await JSRuntime.RemoveWebAuthnConfigured();
-    }
 }

From 3f79a37d084aa83de29ed9765b197f089550cf75 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Wed, 12 Mar 2025 12:39:09 +0330
Subject: [PATCH 11/20] remove existing keys from create option

---
 .../Controllers/Identity/UserController.WebAuthn.cs             | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
index 7fb0c7f0a6..d1afe01211 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
@@ -47,7 +47,7 @@ public async Task<CredentialCreateOptions> GetWebAuthnCredentialOptions(Cancella
         var options = fido2.RequestNewCredential(new RequestNewCredentialParams
         {
             User = fidoUser,
-            ExcludeCredentials = [.. existingKeys],
+            ExcludeCredentials = [], //[.. existingKeys],
             AuthenticatorSelection = authenticatorSelection,
             AttestationPreference = AttestationConveyancePreference.None,
             Extensions = extensions

From 8b7330b17e69153bba50f999e7edc983d21b5c0f Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Wed, 12 Mar 2025 16:54:32 +0330
Subject: [PATCH 12/20] fix tfa issues

---
 .../Pages/Identity/SignIn/SignInPage.razor.cs | 44 +++++++++++++------
 .../Identity/IdentityController.WebAuthn.cs   | 31 +++++++++----
 .../Identity/IdentityController.cs            | 11 ++++-
 .../Identity/UserController.WebAuthn.cs       |  2 +-
 .../Identity/IIdentityController.cs           |  5 ++-
 .../src/Shared/Dtos/AppJsonContext.cs         |  2 +
 .../Identity/VerifyWebAuthnAndSignInDto.cs    | 10 +++++
 7 files changed, 80 insertions(+), 25 deletions(-)
 create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/VerifyWebAuthnAndSignInDto.cs

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs
index 62692817c0..d85acfe039 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs
@@ -38,6 +38,7 @@ public partial class SignInPage
     private SignInPanelTab currentSignInPanelTab;
     private readonly SignInRequestDto model = new();
     private AppDataAnnotationsValidator? validatorRef;
+    private AuthenticatorAssertionRawResponse? webAuthnAssertion;
     private Action unsubscribeIdentityHeaderBackLinkClicked = default!;
 
 
@@ -105,20 +106,29 @@ private async Task DoSignIn()
         {
             if (requiresTwoFactor && string.IsNullOrWhiteSpace(model.TwoFactorCode)) return;
 
-            CleanModel();
+            if (webAuthnAssertion is null)
+            {
+                CleanModel();
 
-            if (validatorRef?.EditContext.Validate() is false) return;
+                if (validatorRef?.EditContext.Validate() is false) return;
 
-            model.DeviceInfo = telemetryContext.Platform;
+                model.DeviceInfo = telemetryContext.Platform;
 
-            requiresTwoFactor = await AuthManager.SignIn(model, CurrentCancellationToken);
+                requiresTwoFactor = await AuthManager.SignIn(model, CurrentCancellationToken);
 
-            if (requiresTwoFactor)
-            {
-                PubSubService.Publish(ClientPubSubMessages.UPDATE_IDENTITY_HEADER_BACK_LINK, TfaPayload);
+                if (requiresTwoFactor)
+                {
+                    PubSubService.Publish(ClientPubSubMessages.UPDATE_IDENTITY_HEADER_BACK_LINK, TfaPayload);
+                }
+                else
+                {
+                    NavigationManager.NavigateTo(ReturnUrlQueryString ?? Urls.HomePage, replace: true);
+                }
             }
             else
             {
+                var response = await identityController.VerifyWebAuthAndSignIn(new() { ClientResponse = webAuthnAssertion, TfaCode = model.TwoFactorCode }, CurrentCancellationToken);
+                await AuthManager.StoreTokens(response!, model.RememberMe);
                 NavigationManager.NavigateTo(ReturnUrlQueryString ?? Urls.HomePage, replace: true);
             }
         }
@@ -160,10 +170,9 @@ private async Task HandleOnPasswordlessSignIn()
         {
             var options = await identityController.GetWebAuthnAssertionOptions(CurrentCancellationToken);
 
-            AuthenticatorAssertionRawResponse assertion;
             try
             {
-                assertion = await JSRuntime.VerifyWebAuthnCredential(options);
+                webAuthnAssertion = await JSRuntime.VerifyWebAuthnCredential(options);
             }
             catch (Exception ex)
             {
@@ -172,9 +181,11 @@ private async Task HandleOnPasswordlessSignIn()
                 return;
             }
 
-            var response = await identityController.VerifyWebAuthAndSignIn(assertion, CurrentCancellationToken);
+            var response = await identityController.VerifyWebAuthAndSignIn(new() { ClientResponse = webAuthnAssertion }, CurrentCancellationToken);
+
+            requiresTwoFactor = response.RequiresTwoFactor;
 
-            if (response.RequiresTwoFactor)
+            if (requiresTwoFactor)
             {
                 PubSubService.Publish(ClientPubSubMessages.UPDATE_IDENTITY_HEADER_BACK_LINK, TfaPayload);
             }
@@ -242,9 +253,16 @@ private async Task HandleOnSendTfaToken()
     {
         try
         {
-            CleanModel();
+            if (webAuthnAssertion is null)
+            {
+                CleanModel();
 
-            await identityController.SendTwoFactorToken(model, CurrentCancellationToken);
+                await identityController.SendTwoFactorToken(model, CurrentCancellationToken);
+            }
+            else
+            {
+                await identityController.VerifyWebAuthAndSendTwoFactorToken(webAuthnAssertion, CurrentCancellationToken);
+            }
 
             SnackBarService.Success(Localizer[nameof(AppStrings.TfaTokenSentMessage)]);
         }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
index f677138a13..6f7aadb423 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
@@ -34,7 +34,7 @@ public async Task<AssertionOptions> GetWebAuthnAssertionOptions(CancellationToke
         });
 
         var key = new string([.. options.Challenge.Select(b => (char)b)]);
-        await cache.SetAsync(key, Encoding.UTF8.GetBytes(options.ToJson()), cancellationToken);
+        await cache.SetAsync(key, Encoding.UTF8.GetBytes(options.ToJson()), new() { SlidingExpiration = TimeSpan.FromMinutes(3) }, cancellationToken);
 
         return options;
     }
@@ -48,22 +48,36 @@ public async Task<VerifyAssertionResult> VerifyWebAuthAssertion(AuthenticatorAss
     }
 
     [HttpPost, Produces<SignInResponseDto>()]
-    public async Task VerifyWebAuthAndSignIn(AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken)
+    public async Task VerifyWebAuthAndSignIn(VerifyWebAuthnAndSignInDto request, CancellationToken cancellationToken)
     {
-        var (verifyResult, credential) = await Verify(clientResponse, cancellationToken);
+        var (verifyResult, credential) = await Verify(request.ClientResponse, cancellationToken);
 
         var user = await userManager.FindByIdAsync(credential.UserId.ToString())
                     ?? throw new ResourceNotFoundException();
 
         var (otp, _) = await GenerateAutomaticSignInLink(user, null, "WebAuthn");
 
-        credential.SignCount = verifyResult.SignCount;
+        if (user.TwoFactorEnabled is false || request.TfaCode is not null)
+        {
+            credential.SignCount = verifyResult.SignCount;
+            DbContext.WebAuthnCredential.Update(credential);
+            await DbContext.SaveChangesAsync(cancellationToken);
+        }
 
-        DbContext.WebAuthnCredential.Update(credential);
+        await SignIn(new() { Otp = otp, TwoFactorCode = request.TfaCode }, user, cancellationToken);
+    }
 
-        await DbContext.SaveChangesAsync(cancellationToken);
+    [HttpPost]
+    public async Task VerifyWebAuthAndSendTwoFactorToken(AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken)
+    {
+        var (verifyResult, credential) = await Verify(clientResponse, cancellationToken);
+
+        var user = await userManager.FindByIdAsync(credential.UserId.ToString())
+                    ?? throw new ResourceNotFoundException();
+
+        var (otp, _) = await GenerateAutomaticSignInLink(user, null, "WebAuthn");
 
-        await SignIn(new() { Otp = otp }, user, cancellationToken);
+        await SendTwoFactorToken(new() { Otp = otp }, user, cancellationToken);
     }
 
 
@@ -79,7 +93,8 @@ public async Task VerifyWebAuthAndSignIn(AuthenticatorAssertionRawResponse clien
         var jsonOptions = Encoding.UTF8.GetString(cachedBytes);
         var options = AssertionOptions.FromJson(jsonOptions);
 
-        await cache.RemoveAsync(key, cancellationToken);
+        // since the TFA needs this option we won't remove it from cache manually and just wait for it to expire.
+        // await cache.RemoveAsync(key, cancellationToken);
 
         var credential = (await DbContext.WebAuthnCredential.FirstOrDefaultAsync(c => c.Id == clientResponse.Id, cancellationToken))
                             ?? throw new ResourceNotFoundException();
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs
index 353d70da57..daf1a5cd63 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs
@@ -92,7 +92,7 @@ public async Task SignIn(SignInRequestDto request, CancellationToken cancellatio
     {
         request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
 
-        var user = await userManager.FindUserAsync(request) 
+        var user = await userManager.FindUserAsync(request)
                     ?? throw new UnauthorizedException(Localizer[nameof(AppStrings.InvalidUserCredentials)]).WithData("Identifier", request);
 
         await SignIn(request, user, cancellationToken);
@@ -341,8 +341,15 @@ public async Task SendOtp(IdentityRequestDto request, string? returnUrl = null,
     public async Task SendTwoFactorToken(SignInRequestDto request, CancellationToken cancellationToken)
     {
         request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
-        var user = await userManager.FindUserAsync(request) ?? throw new ResourceNotFoundException(Localizer[nameof(AppStrings.UserNotFound)]).WithData("Identifier", request);
 
+        var user = await userManager.FindUserAsync(request)
+                    ?? throw new ResourceNotFoundException(Localizer[nameof(AppStrings.UserNotFound)]).WithData("Identifier", request);
+
+        await SendTwoFactorToken(request, user, cancellationToken);
+    }
+
+    private async Task SendTwoFactorToken(SignInRequestDto request, User user, CancellationToken cancellationToken)
+    {
         if (user.TwoFactorEnabled is false)
             throw new BadRequestException().WithData("UserId", user.Id);
 
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
index d1afe01211..05709f5efb 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
@@ -54,7 +54,7 @@ public async Task<CredentialCreateOptions> GetWebAuthnCredentialOptions(Cancella
         });
 
         var key = GetWebAuthnCacheKey(userId);
-        await cache.SetAsync(key, Encoding.UTF8.GetBytes(options.ToJson()), cancellationToken);
+        await cache.SetAsync(key, Encoding.UTF8.GetBytes(options.ToJson()), new() { SlidingExpiration = TimeSpan.FromMinutes(3) }, cancellationToken);
 
         return options;
     }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IIdentityController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IIdentityController.cs
index 81ac52e4c2..aa2b7b44b0 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IIdentityController.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IIdentityController.cs
@@ -55,5 +55,8 @@ public interface IIdentityController : IAppController
     Task<VerifyAssertionResult> VerifyWebAuthAssertion(AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken);
 
     [HttpPost]
-    Task<SignInResponseDto> VerifyWebAuthAndSignIn(AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken) => default!;
+    Task<SignInResponseDto> VerifyWebAuthAndSignIn(VerifyWebAuthnAndSignInDto request, CancellationToken cancellationToken) => default!;
+
+    [HttpPost]
+    Task VerifyWebAuthAndSendTwoFactorToken(AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken);
 }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs
index 43783c773a..c28ab09a1e 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs
@@ -14,6 +14,7 @@
 //#if (notification == true)
 using Boilerplate.Shared.Dtos.PushNotification;
 //#endif
+using Boilerplate.Shared.Dtos.Identity;
 using Boilerplate.Shared.Dtos.Statistics;
 
 namespace Boilerplate.Shared.Dtos;
@@ -55,6 +56,7 @@ namespace Boilerplate.Shared.Dtos;
 [JsonSerializable(typeof(AuthenticatorAttestationRawResponse))]
 [JsonSerializable(typeof(CredentialCreateOptions))]
 [JsonSerializable(typeof(VerifyAssertionResult))]
+[JsonSerializable(typeof(VerifyWebAuthnAndSignInDto))]
 public partial class AppJsonContext : JsonSerializerContext
 {
 }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/VerifyWebAuthnAndSignInDto.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/VerifyWebAuthnAndSignInDto.cs
new file mode 100644
index 0000000000..65508939d0
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/VerifyWebAuthnAndSignInDto.cs
@@ -0,0 +1,10 @@
+using Fido2NetLib;
+
+namespace Boilerplate.Shared.Dtos.Identity;
+
+public partial class VerifyWebAuthnAndSignInDto
+{
+    public required AuthenticatorAssertionRawResponse ClientResponse { get; set; }
+
+    public string? TfaCode { get; set; }
+}

From 542550395e5d5c5ea76dfb64bf0a3e6b3281966e Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Thu, 13 Mar 2025 07:17:45 +0330
Subject: [PATCH 13/20] reduce authenticator selection options

---
 .../Pages/Identity/SignIn/SignInPage.razor.cs         |  2 ++
 .../Controllers/Identity/UserController.WebAuthn.cs   | 11 ++++++++---
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs
index d85acfe039..d257bf26c0 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs
@@ -84,6 +84,7 @@ protected override async Task OnInitAsync()
 
             if (source == TfaPayload)
             {
+                webAuthnAssertion = null;
                 requiresTwoFactor = false;
                 model.TwoFactorCode = null;
             }
@@ -197,6 +198,7 @@ private async Task HandleOnPasswordlessSignIn()
         }
         catch (KnownException e)
         {
+            webAuthnAssertion = null;
             SnackBarService.Error(e.Message);
         }
         finally
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
index 05709f5efb..49930b4171 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
@@ -31,10 +31,15 @@ public async Task<CredentialCreateOptions> GetWebAuthnCredentialOptions(Cancella
             DisplayName = user.DisplayName,
         };
 
+        //var authenticatorSelection = new AuthenticatorSelection
+        //{
+        //    ResidentKey = ResidentKeyRequirement.Required,
+        //    UserVerification = UserVerificationRequirement.Preferred
+        //};
+
         var authenticatorSelection = new AuthenticatorSelection
         {
-            ResidentKey = ResidentKeyRequirement.Required,
-            UserVerification = UserVerificationRequirement.Preferred
+            AuthenticatorAttachment = AuthenticatorAttachment.Platform
         };
 
         var extensions = new AuthenticationExtensionsClientInputs
@@ -50,7 +55,7 @@ public async Task<CredentialCreateOptions> GetWebAuthnCredentialOptions(Cancella
             ExcludeCredentials = [], //[.. existingKeys],
             AuthenticatorSelection = authenticatorSelection,
             AttestationPreference = AttestationConveyancePreference.None,
-            Extensions = extensions
+            //Extensions = extensions
         });
 
         var key = GetWebAuthnCacheKey(userId);

From d5c9b4c560463ef46a5a0ba3c0d8563b31415ccb Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Thu, 13 Mar 2025 07:59:36 +0330
Subject: [PATCH 14/20] reduce assertion options

---
 .../src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts    | 4 ++--
 .../Controllers/Identity/IdentityController.WebAuthn.cs       | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
index ab1b60354b..b24b327f50 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/WebAuthn.ts
@@ -27,9 +27,9 @@ class WebAuthn {
 
         options.user.id = WebAuthn.ToArrayBuffer(options.user.id, 'user.id');
 
-        for (let cred of options.excludeCredentials || []) {
+        options.excludeCredentials?.forEach(function (cred) {
             cred.id = WebAuthn.ToArrayBuffer(cred.id, 'cred.id');
-        }
+        });
 
         if (options.authenticatorSelection?.authenticatorAttachment === null) {
             options.authenticatorSelection.authenticatorAttachment = undefined;
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
index 6f7aadb423..21485f8886 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
@@ -28,8 +28,8 @@ public async Task<AssertionOptions> GetWebAuthnAssertionOptions(CancellationToke
 
         var options = fido2.GetAssertionOptions(new GetAssertionOptionsParams
         {
-            Extensions = extensions,
-            AllowedCredentials = existingKeys,
+            //Extensions = extensions,
+            //AllowedCredentials = existingKeys,
             UserVerification = UserVerificationRequirement.Discouraged,
         });
 

From 53330a291103eaac3c80eb763cd8eb922811a833 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Thu, 13 Mar 2025 08:50:44 +0330
Subject: [PATCH 15/20] restore extensions options back

---
 .../Controllers/Identity/IdentityController.WebAuthn.cs       | 4 ++--
 .../Controllers/Identity/UserController.WebAuthn.cs           | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
index 21485f8886..6f7aadb423 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.WebAuthn.cs
@@ -28,8 +28,8 @@ public async Task<AssertionOptions> GetWebAuthnAssertionOptions(CancellationToke
 
         var options = fido2.GetAssertionOptions(new GetAssertionOptionsParams
         {
-            //Extensions = extensions,
-            //AllowedCredentials = existingKeys,
+            Extensions = extensions,
+            AllowedCredentials = existingKeys,
             UserVerification = UserVerificationRequirement.Discouraged,
         });
 
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
index 49930b4171..df60dfe2fc 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
@@ -55,7 +55,7 @@ public async Task<CredentialCreateOptions> GetWebAuthnCredentialOptions(Cancella
             ExcludeCredentials = [], //[.. existingKeys],
             AuthenticatorSelection = authenticatorSelection,
             AttestationPreference = AttestationConveyancePreference.None,
-            //Extensions = extensions
+            Extensions = extensions
         });
 
         var key = GetWebAuthnCacheKey(userId);

From 807332be091a4c48d7311312056cc3cc6dd24c09 Mon Sep 17 00:00:00 2001
From: ysmoradi <ysmoradi@outlook.com>
Date: Thu, 13 Mar 2025 07:31:08 +0100
Subject: [PATCH 16/20] trusted origins

---
 .../Program.Services.cs                       | 34 +++++++++++--------
 .../ServerApiSettings.cs                      | 10 +++---
 .../Boilerplate.Server.Api/appsettings.json   |  4 +--
 3 files changed, 26 insertions(+), 22 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
index b6cdbdf3f8..a9484f56ea 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
@@ -10,6 +10,7 @@
 using Microsoft.AspNetCore.ResponseCompression;
 using System.Security.Cryptography.X509Certificates;
 using Twilio;
+using Fido2NetLib;
 using PhoneNumbers;
 using FluentStorage;
 using FluentStorage.Blobs;
@@ -22,7 +23,6 @@
 using Boilerplate.Server.Api.Controllers;
 using Boilerplate.Server.Api.Models.Identity;
 using Boilerplate.Server.Api.Services.Identity;
-
 namespace Boilerplate.Server.Api;
 
 public static partial class Program
@@ -301,20 +301,24 @@ void AddDbContext(DbContextOptionsBuilder options)
 
         services.AddFido2(options =>
         {
-            var trustedOrigins = appSettings.AllowedOrigins
-                .Union([
-                    //#if (api == "Integrated")
-                    new Uri("http://localhost:5030/")
-                    //#else
-                    , new Uri("http://localhost:5031/")
-                    //#endif
-                ]);
-
-            options.TimestampDriftTolerance = 1000;
-            options.ServerName = "Boilerplate WebAuthn";
-            options.ServerDomain = trustedOrigins.First().Host;
-            options.Origins = new HashSet<string>(trustedOrigins.Select(uri => uri.AbsoluteUri));
-            options.ServerIcon = "data:image/png;base64,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";
+
+        });
+
+        services.AddScoped(sp =>
+        {
+            var webAppUrl = sp.GetRequiredService<IHttpContextAccessor>()
+                .HttpContext!.Request.GetWebAppUrl();
+
+            var options = new Fido2Configuration
+            {
+                TimestampDriftTolerance = 1000,
+                ServerName = "Boilerplate WebAuthn",
+                ServerDomain = webAppUrl.Host,
+                Origins = new HashSet<string>([webAppUrl.AbsoluteUri]),
+                ServerIcon = "data:image/png;base64,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"
+            };
+
+            return options;
         });
     }
 
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs
index 306157b6c0..606a98beef 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs
@@ -48,9 +48,9 @@ public partial class ServerApiSettings : SharedSettings
     public ResponseCachingOptions ResponseCaching { get; set; } = default!;
 
     /// <summary>
-    /// Defines the list of origins permitted for CORS access to the API. These origins are `also` valid for use as return URLs after social sign-ins and for generating URLs in emails.
+    /// Lists the permitted origins for CORS requests, return URLs following social sign-in and email confirmation, etc., along with allowed origins for Web Auth.
     /// </summary>
-    public Uri[] AllowedOrigins { get; set; } = [];
+    public Uri[] TrustedOrigins { get; set; } = [];
 
     //#if (module == "Admin" || module == "Sales")
     [Required]
@@ -113,8 +113,8 @@ public override IEnumerable<ValidationResult> Validate(ValidationContext validat
 
     internal bool IsAllowedOrigin(Uri origin)
     {
-        return AllowedOrigins.Any(allowedOrigin => allowedOrigin == origin)
-            || AllowedOriginsRegex().IsMatch(origin.ToString());
+        return TrustedOrigins.Any(trustedOrigin => trustedOrigin == origin)
+            || TrustedOriginsRegex().IsMatch(origin.ToString());
     }
 
     //-:cnd:noEmit
@@ -127,7 +127,7 @@ internal bool IsAllowedOrigin(Uri origin)
     [GeneratedRegex(@"^(http|https|app):\/\/(localhost|0\.0\.0\.0|0\.0\.0\.1|127\.0\.0\.1)(:\d+)?(\/.*)?$")]
 #endif
     //+:cnd:noEmit
-    private partial Regex AllowedOriginsRegex();
+    private partial Regex TrustedOriginsRegex();
 }
 
 public partial class AppIdentityOptions : IdentityOptions
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json
index c0ce66bac6..225695696c 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json
@@ -131,8 +131,8 @@
         }
     },
     "AllowedHosts": "*",
-    "AllowedOrigins": [],
-    "AllowedOrigins_Comment": "Defines the list of origins permitted for CORS access to the API. These origins are also valid for use as return URLs after social sign-ins and for generating URLs in emails.",
+    "TrustedOrigins": [],
+    "TrustedOrigins_Comment": "Lists the permitted origins for CORS requests, return URLs following social sign-in and email confirmation, etc., along with allowed origins for Web Auth.",
     "ForwardedHeaders": {
         "ForwardedHeaders": "All",
         "ForwardedHeaders_Comment": "These values apply only if your backend is hosted behind a CDN (such as `Cloudflare`).",

From 43e3625bbd12a87454b2ad1f41e2ee94ce1c3529 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Thu, 13 Mar 2025 11:20:10 +0330
Subject: [PATCH 17/20] change servericon value to url

---
 .../wwwroot/images/icons/bit-logo.png            | Bin 0 -> 2463 bytes
 .../Identity/UserController.WebAuthn.cs          |  14 +++++++-------
 .../Boilerplate.Server.Api/Program.Services.cs   |   5 +++--
 3 files changed, 10 insertions(+), 9 deletions(-)
 create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/images/icons/bit-logo.png

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/images/icons/bit-logo.png b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/images/icons/bit-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..1629cbe86b5305df2fb933cbae2cfb6edde90fab
GIT binary patch
literal 2463
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

literal 0
HcmV?d00001

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
index df60dfe2fc..910c1db097 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.WebAuthn.cs
@@ -31,17 +31,17 @@ public async Task<CredentialCreateOptions> GetWebAuthnCredentialOptions(Cancella
             DisplayName = user.DisplayName,
         };
 
-        //var authenticatorSelection = new AuthenticatorSelection
-        //{
-        //    ResidentKey = ResidentKeyRequirement.Required,
-        //    UserVerification = UserVerificationRequirement.Preferred
-        //};
-
         var authenticatorSelection = new AuthenticatorSelection
         {
-            AuthenticatorAttachment = AuthenticatorAttachment.Platform
+            ResidentKey = ResidentKeyRequirement.Required,
+            UserVerification = UserVerificationRequirement.Preferred
         };
 
+        //var authenticatorSelection = new AuthenticatorSelection
+        //{
+        //    AuthenticatorAttachment = AuthenticatorAttachment.Platform
+        //};
+
         var extensions = new AuthenticationExtensionsClientInputs
         {
             CredProps = true,
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
index a9484f56ea..d713868757 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
@@ -222,7 +222,8 @@ void AddDbContext(DbContextOptionsBuilder options)
             //#elif (database == "Other")
             throw new NotImplementedException("Install and configure any database supported by ef core (https://learn.microsoft.com/en-us/ef/core/providers)");
             //#endif
-        };
+        }
+        ;
 
         services.AddOptions<IdentityOptions>()
             .Bind(configuration.GetRequiredSection(nameof(ServerApiSettings.Identity)))
@@ -315,7 +316,7 @@ void AddDbContext(DbContextOptionsBuilder options)
                 ServerName = "Boilerplate WebAuthn",
                 ServerDomain = webAppUrl.Host,
                 Origins = new HashSet<string>([webAppUrl.AbsoluteUri]),
-                ServerIcon = "data:image/png;base64,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"
+                ServerIcon = new Uri(webAppUrl, "images/icons/bit-logo.png").ToString()
             };
 
             return options;

From 897c9473b80814116cdd323e5584a10540e25d24 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Thu, 13 Mar 2025 11:34:26 +0330
Subject: [PATCH 18/20] remove redundant storage item removal

---
 .../Components/Layout/AppDiagnosticModal.razor.Utils.cs         | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
index cc7d9e607e..ba9a535897 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
@@ -124,7 +124,7 @@ private async Task ClearData()
         {
             await userController.DeleteAllWebAuthnCredentials(CurrentCancellationToken);
 
-            await JSRuntime.RemoveWebAuthnConfigured();
+            //await JSRuntime.RemoveWebAuthnConfigured();
         }
 
         if (AppPlatform.IsBlazorHybrid is false)

From 8eca1c80bfa1b7e0fa2fc74aab89d1d1fba4bb7a Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Thu, 13 Mar 2025 13:13:49 +0330
Subject: [PATCH 19/20] fix review comments

---
 .../Components/Layout/AppDiagnosticModal.razor.Utils.cs        | 2 ++
 .../src/Server/Boilerplate.Server.Api/Program.Services.cs      | 3 +--
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
index ba9a535897..a727ff7353 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
@@ -124,6 +124,8 @@ private async Task ClearData()
         {
             await userController.DeleteAllWebAuthnCredentials(CurrentCancellationToken);
 
+            // since the localstorage is used to store configured webauthn users and it is already cleared above, we can ignore the following line.
+            // we kept it commented for future refrences.
             //await JSRuntime.RemoveWebAuthnConfigured();
         }
 
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
index d713868757..207fa1b314 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs
@@ -222,8 +222,7 @@ void AddDbContext(DbContextOptionsBuilder options)
             //#elif (database == "Other")
             throw new NotImplementedException("Install and configure any database supported by ef core (https://learn.microsoft.com/en-us/ef/core/providers)");
             //#endif
-        }
-        ;
+        };
 
         services.AddOptions<IdentityOptions>()
             .Bind(configuration.GetRequiredSection(nameof(ServerApiSettings.Identity)))

From a3d6765627b53fa6a6d22920f6779180099ab167 Mon Sep 17 00:00:00 2001
From: Saleh Yusefnejad <msynk@outlook.com>
Date: Thu, 13 Mar 2025 13:14:52 +0330
Subject: [PATCH 20/20] fix typo

---
 .../Components/Layout/AppDiagnosticModal.razor.Utils.cs         | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
index a727ff7353..a218c0e5cf 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs
@@ -124,7 +124,7 @@ private async Task ClearData()
         {
             await userController.DeleteAllWebAuthnCredentials(CurrentCancellationToken);
 
-            // since the localstorage is used to store configured webauthn users and it is already cleared above, we can ignore the following line.
+            // since the localStorage is used to store configured webauthn users and it is already cleared above, we can ignore the following line.
             // we kept it commented for future refrences.
             //await JSRuntime.RemoveWebAuthnConfigured();
         }