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