Skip to content

Commit af8bb32

Browse files
authored
fix(templates): resolve WebAuthn issues in Boilerplate bitfoundation#10228 (bitfoundation#10229)
1 parent d949ec4 commit af8bb32

File tree

20 files changed

+290
-117
lines changed

20 files changed

+290
-117
lines changed

src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor

+2-2
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@
8888
IconName="@BitIconName.RecycleBin" />
8989
}
9090
<BitButton IconOnly AutoLoading
91-
OnClick="ClearCache"
92-
Title="Clear cache"
91+
OnClick="ClearData"
92+
Title="Clear data"
9393
Color="BitColor.SecondaryBackground"
9494
IconName="@BitIconName.Clear" />
9595
</BitStack>

src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppDiagnosticModal.razor.Utils.cs

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Text;
22
using System.Diagnostics;
33
using System.Runtime.CompilerServices;
4+
using Boilerplate.Shared.Controllers.Identity;
45
//#if (signalR == true)
56
using Microsoft.AspNetCore.SignalR.Client;
67
//#endif
@@ -9,9 +10,10 @@ namespace Boilerplate.Client.Core.Components.Layout;
910

1011
public partial class AppDiagnosticModal
1112
{
12-
[AutoInject] Cookie cookie = default!;
13-
[AutoInject] AuthManager authManager = default!;
14-
[AutoInject] IStorageService storageService = default!;
13+
[AutoInject] private Cookie cookie = default!;
14+
[AutoInject] private AuthManager authManager = default!;
15+
[AutoInject] private IStorageService storageService = default!;
16+
[AutoInject] private IUserController userController = default!;
1517

1618
private static async Task ThrowTestException()
1719
{
@@ -97,13 +99,13 @@ await Task.Run(() =>
9799
SnackBarService.Show("Memory After GC", GetMemoryUsage());
98100
}
99101

100-
string GetMemoryUsage()
102+
private string GetMemoryUsage()
101103
{
102104
long memory = Environment.WorkingSet;
103105
return $"{memory / (1024.0 * 1024.0):F2} MB";
104106
}
105107

106-
async Task ClearCache()
108+
private async Task ClearData()
107109
{
108110
try
109111
{
@@ -118,6 +120,15 @@ async Task ClearCache()
118120
await cookie.Remove(item.Name!);
119121
}
120122

123+
if ((await AuthenticationStateTask).User.IsAuthenticated())
124+
{
125+
await userController.DeleteAllWebAuthnCredentials(CurrentCancellationToken);
126+
127+
// since the localStorage is used to store configured webauthn users and it is already cleared above, we can ignore the following line.
128+
// we kept it commented for future refrences.
129+
//await JSRuntime.RemoveWebAuthnConfigured();
130+
}
131+
121132
if (AppPlatform.IsBlazorHybrid is false)
122133
{
123134
await JSRuntime.InvokeVoidAsync("BitBswup.forceRefresh"); // Clears cache storages and uninstalls service-worker.

src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/PasswordlessSection.razor

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
<br />
99
@if (isConfigured)
1010
{
11-
<BitButton OnClick="WrapHandled(DisablePasswordless)" Variant="BitVariant.Outline" Color="BitColor.Warning">
11+
<BitButton AutoLoading OnClick="WrapHandled(DisablePasswordless)" Variant="BitVariant.Outline" Color="BitColor.Warning">
1212
@Localizer[nameof(AppStrings.DisablePasswordless)]
1313
</BitButton>
1414
}
1515
else
1616
{
17-
<BitButton OnClick="WrapHandled(EnablePasswordless)">
17+
<BitButton AutoLoading OnClick="WrapHandled(EnablePasswordless)">
1818
@Localizer[nameof(AppStrings.EnablePasswordless)]
1919
</BitButton>
2020
}

src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ protected override async Task OnInitAsync()
3434
try
3535
{
3636
user = (await PrerenderStateService.GetValue(() => HttpClient.GetFromJsonAsync("api/User/GetCurrentUser", JsonSerializerOptions.GetTypeInfo<UserDto>(), CurrentCancellationToken)))!;
37-
showPasswordless = await JSRuntime.IsWebAuthnAvailable() && AppPlatform.IsBlazorHybrid is false;
37+
if (InPrerenderSession is false)
38+
{
39+
showPasswordless = await JSRuntime.IsWebAuthnAvailable() && AppPlatform.IsBlazorHybrid is false;
40+
}
3841
}
3942
finally
4043
{

src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs

+33-13
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public partial class SignInPage
3838
private SignInPanelTab currentSignInPanelTab;
3939
private readonly SignInRequestDto model = new();
4040
private AppDataAnnotationsValidator? validatorRef;
41+
private AuthenticatorAssertionRawResponse? webAuthnAssertion;
4142
private Action unsubscribeIdentityHeaderBackLinkClicked = default!;
4243

4344

@@ -83,6 +84,7 @@ protected override async Task OnInitAsync()
8384

8485
if (source == TfaPayload)
8586
{
87+
webAuthnAssertion = null;
8688
requiresTwoFactor = false;
8789
model.TwoFactorCode = null;
8890
}
@@ -105,20 +107,29 @@ private async Task DoSignIn()
105107
{
106108
if (requiresTwoFactor && string.IsNullOrWhiteSpace(model.TwoFactorCode)) return;
107109

108-
CleanModel();
110+
if (webAuthnAssertion is null)
111+
{
112+
CleanModel();
109113

110-
if (validatorRef?.EditContext.Validate() is false) return;
114+
if (validatorRef?.EditContext.Validate() is false) return;
111115

112-
model.DeviceInfo = telemetryContext.Platform;
116+
model.DeviceInfo = telemetryContext.Platform;
113117

114-
requiresTwoFactor = await AuthManager.SignIn(model, CurrentCancellationToken);
118+
requiresTwoFactor = await AuthManager.SignIn(model, CurrentCancellationToken);
115119

116-
if (requiresTwoFactor)
117-
{
118-
PubSubService.Publish(ClientPubSubMessages.UPDATE_IDENTITY_HEADER_BACK_LINK, TfaPayload);
120+
if (requiresTwoFactor)
121+
{
122+
PubSubService.Publish(ClientPubSubMessages.UPDATE_IDENTITY_HEADER_BACK_LINK, TfaPayload);
123+
}
124+
else
125+
{
126+
NavigationManager.NavigateTo(ReturnUrlQueryString ?? Urls.HomePage, replace: true);
127+
}
119128
}
120129
else
121130
{
131+
var response = await identityController.VerifyWebAuthAndSignIn(new() { ClientResponse = webAuthnAssertion, TfaCode = model.TwoFactorCode }, CurrentCancellationToken);
132+
await AuthManager.StoreTokens(response!, model.RememberMe);
122133
NavigationManager.NavigateTo(ReturnUrlQueryString ?? Urls.HomePage, replace: true);
123134
}
124135
}
@@ -160,10 +171,9 @@ private async Task HandleOnPasswordlessSignIn()
160171
{
161172
var options = await identityController.GetWebAuthnAssertionOptions(CurrentCancellationToken);
162173

163-
AuthenticatorAssertionRawResponse assertion;
164174
try
165175
{
166-
assertion = await JSRuntime.VerifyWebAuthnCredential(options);
176+
webAuthnAssertion = await JSRuntime.VerifyWebAuthnCredential(options);
167177
}
168178
catch (Exception ex)
169179
{
@@ -172,9 +182,11 @@ private async Task HandleOnPasswordlessSignIn()
172182
return;
173183
}
174184

175-
var response = await identityController.VerifyWebAuthAndSignIn(assertion, CurrentCancellationToken);
185+
var response = await identityController.VerifyWebAuthAndSignIn(new() { ClientResponse = webAuthnAssertion }, CurrentCancellationToken);
186+
187+
requiresTwoFactor = response.RequiresTwoFactor;
176188

177-
if (response.RequiresTwoFactor)
189+
if (requiresTwoFactor)
178190
{
179191
PubSubService.Publish(ClientPubSubMessages.UPDATE_IDENTITY_HEADER_BACK_LINK, TfaPayload);
180192
}
@@ -186,6 +198,7 @@ private async Task HandleOnPasswordlessSignIn()
186198
}
187199
catch (KnownException e)
188200
{
201+
webAuthnAssertion = null;
189202
SnackBarService.Error(e.Message);
190203
}
191204
finally
@@ -242,9 +255,16 @@ private async Task HandleOnSendTfaToken()
242255
{
243256
try
244257
{
245-
CleanModel();
258+
if (webAuthnAssertion is null)
259+
{
260+
CleanModel();
246261

247-
await identityController.SendTwoFactorToken(model, CurrentCancellationToken);
262+
await identityController.SendTwoFactorToken(model, CurrentCancellationToken);
263+
}
264+
else
265+
{
266+
await identityController.VerifyWebAuthAndSendTwoFactorToken(webAuthnAssertion, CurrentCancellationToken);
267+
}
248268

249269
SnackBarService.Success(Localizer[nameof(AppStrings.TfaTokenSentMessage)]);
250270
}

src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeWebAuthnExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static ValueTask<bool> IsWebAuthnConfigured(this IJSRuntime jsRuntime, st
2020
return jsRuntime.InvokeAsync<bool>("WebAuthn.isConfigured", username);
2121
}
2222

23-
public static ValueTask RemoveWebAuthnConfigured(this IJSRuntime jsRuntime, string username)
23+
public static ValueTask RemoveWebAuthnConfigured(this IJSRuntime jsRuntime, string? username = null)
2424
{
2525
return jsRuntime.InvokeVoidAsync("WebAuthn.removeConfigured", username);
2626
}

0 commit comments

Comments
 (0)