Skip to content

Commit e4ba445

Browse files
authored
Fix Blazor template bug where a logged in user could appear to be unauthenticated (#51497)
## Description A customer reported that after enhanced navigation from a WebAssembly-rendered component that does not require authentication state to another WebAssembly-rendered component that does, the client will lose its authentication state. This leads to issues where the server knows the client is authenticated meaning the client can navigate to components that require authentication, but the client thinks the user is unauthenticated for the purposes of rendering something like: ```razor <AuthorizeView> <Authorized> <p>You are authorized</p> Hello @context.User.Identity?.Name! </Authorized> <NotAuthorized> <p>You are not authorized</p> </NotAuthorized> </AuthorizeView> ``` This was caused by the `PersistentAuthenticationStateProvider` on the client attempting to read the `UserInfo` from `PersistentComponentState` after enhanced navigation. At this point the state from the original page load (`/counter` in the examples below) is cleared, and the state that was persisted during the enhanced navigation to `/auth` is simply ignored and never read by anyone despite calling the `RegisterOnPersisting` callback and rendering the state for each enhanced navigation. The `PersistentComponentState` behavior is by design according to @javiercn, and we can make auth work reliably with the current `PersistentComponentState` behavior by greedily reading the `UserInfo` during initial render before any enhanced navigation which is what this PR does. Fixes #51368 ## Customer Impact This was thankfully reported by a customer trying out the new Identity Blazor components in RC2. It's possible to get into this state with just the template code if a browser session starts in (or reload on) the template's `/counter` page and then does enhanced navigation to a component that uses an `<AuthorizeView>` like the template's `/auth` page. Without this fix, the user is forced to refresh the `/auth` page or any other page with `<AuthorizeView>` before they can see any rendered content that requires authorization. https://github.com/dotnet/aspnetcore/assets/54385/742ab1d9-901e-4489-b4d4-183de6423aa5 Notice how the second time I navigate to the "Auth Required" page after refreshing on the "Counter" page, you see "You are authenticated" followed by "You are not authorized" even though any authenticated user should be authorized to see that page. After the fix, you always see "You are authenticated" followed by "You are authorized" and "Hello {email}!" as expected. https://github.com/dotnet/aspnetcore/assets/54385/f8549468-ec68-4dfa-8e8c-800a29e6e349 ## Regression? - [ ] Yes - [x] No ## Risk - [ ] High - [ ] Medium - [x] Low This is a small change to a single Blazor Identity template component added in RC 2 which slightly simplifies the logic. ## Verification - [x] Manual (required) - [ ] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [x] N/A
1 parent 198857c commit e4ba445

File tree

13 files changed

+21
-23
lines changed

13 files changed

+21
-23
lines changed

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,29 @@ namespace BlazorWeb_CSharp.Client;
1212
// This only provides a user name and email for display purposes. It does not actually include any tokens
1313
// that authenticate to the server when making subsequent requests. That works separately using a
1414
// cookie that will be included on HttpClient requests to the server.
15-
public class PersistentAuthenticationStateProvider(PersistentComponentState persistentState) : AuthenticationStateProvider
15+
internal class PersistentAuthenticationStateProvider : AuthenticationStateProvider
1616
{
17-
private static readonly Task<AuthenticationState> unauthenticatedTask =
17+
private static readonly Task<AuthenticationState> defaultUnauthenticatedTask =
1818
Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
1919

20-
public override Task<AuthenticationState> GetAuthenticationStateAsync()
20+
private readonly Task<AuthenticationState> authenticationStateTask = defaultUnauthenticatedTask;
21+
22+
public PersistentAuthenticationStateProvider(PersistentComponentState state)
2123
{
22-
if (!persistentState.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null)
24+
if (!state.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null)
2325
{
24-
return unauthenticatedTask;
26+
return;
2527
}
2628

2729
Claim[] claims = [
2830
new Claim(ClaimTypes.NameIdentifier, userInfo.UserId),
2931
new Claim(ClaimTypes.Name, userInfo.Email),
3032
new Claim(ClaimTypes.Email, userInfo.Email) ];
3133

32-
return Task.FromResult(
34+
authenticationStateTask = Task.FromResult(
3335
new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims,
3436
authenticationType: nameof(PersistentAuthenticationStateProvider)))));
3537
}
38+
39+
public override Task<AuthenticationState> GetAuthenticationStateAsync() => authenticationStateTask;
3640
}

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Login.razor

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
@using System.ComponentModel.DataAnnotations
44
@using Microsoft.AspNetCore.Authentication
55
@using Microsoft.AspNetCore.Identity
6-
@using Microsoft.AspNetCore.WebUtilities
76
@using BlazorWeb_CSharp.Data
87

98
@inject SignInManager<ApplicationUser> SignInManager

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/LoginWithRecoveryCode.razor

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
@using System.ComponentModel.DataAnnotations
44
@using Microsoft.AspNetCore.Identity
5-
@using Microsoft.AspNetCore.Mvc
65
@using BlazorWeb_CSharp.Data
76

87
@inject SignInManager<ApplicationUser> SignInManager

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/ExternalLogins.razor

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
@using Microsoft.AspNetCore.Authentication
44
@using Microsoft.AspNetCore.Identity
5-
@using Microsoft.AspNetCore.Mvc.ViewFeatures
65
@using BlazorWeb_CSharp.Data
76

87
@inject UserManager<ApplicationUser> UserManager

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Manage/Index.razor

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
@page "/Account/Manage"
22

33
@using System.ComponentModel.DataAnnotations
4-
@using System.Security.Claims
54
@using Microsoft.AspNetCore.Identity
65
@using BlazorWeb_CSharp.Data
76

8-
@inject AuthenticationStateProvider AuthenticationStateProvider
97
@inject UserManager<ApplicationUser> UserManager
108
@inject SignInManager<ApplicationUser> SignInManager
119
@inject IdentityUserAccessor UserAccessor

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/Register.razor

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
@using System.ComponentModel.DataAnnotations
44
@using System.Text
55
@using System.Text.Encodings.Web
6-
@using Microsoft.AspNetCore.Authentication
76
@using Microsoft.AspNetCore.Identity
87
@using Microsoft.AspNetCore.WebUtilities
98
@using BlazorWeb_CSharp.Data

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/RegisterConfirmation.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ else
3636
private HttpContext HttpContext { get; set; } = default!;
3737

3838
[SupplyParameterFromQuery]
39-
public string? Email { get; set; }
39+
private string? Email { get; set; }
4040

4141
[SupplyParameterFromQuery]
42-
public string? ReturnUrl { get; set; }
42+
private string? ReturnUrl { get; set; }
4343

4444
protected override async Task OnInitializedAsync()
4545
{

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ResendEmailConfirmation.razor

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
@using System.Text
55
@using System.Text.Encodings.Web
66
@using Microsoft.AspNetCore.Identity
7-
@using Microsoft.AspNetCore.Identity.UI.Services
87
@using Microsoft.AspNetCore.WebUtilities
98
@using BlazorWeb_CSharp.Data
109

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Pages/ResetPassword.razor

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
@using System.ComponentModel.DataAnnotations
44
@using System.Text
5-
@using Microsoft.AspNetCore.Http
65
@using Microsoft.AspNetCore.Identity
76
@using Microsoft.AspNetCore.WebUtilities
87
@using BlazorWeb_CSharp.Data

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/Shared/AccountLayout.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ else
1717

1818
@code {
1919
[CascadingParameter]
20-
public HttpContext? HttpContext { get; set; }
20+
private HttpContext? HttpContext { get; set; }
2121

2222
protected override void OnParametersSet()
2323
{

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/App.razor

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
[CascadingParameter]
5050
private HttpContext HttpContext { get; set; } = default!;
5151
52-
IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
52+
private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
5353
? null
5454
: InteractiveAuto;
5555
}
@@ -59,7 +59,7 @@
5959
[CascadingParameter]
6060
private HttpContext HttpContext { get; set; } = default!;
6161
62-
IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
62+
private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
6363
? null
6464
: InteractiveServer;
6565
}
@@ -69,7 +69,7 @@
6969
[CascadingParameter]
7070
private HttpContext HttpContext { get; set; } = default!;
7171
72-
IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
72+
private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
7373
? null
7474
: InteractiveWebAssembly;
7575
}

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
@*#if (IndividualLocalAuth)
2+
@implements IDisposable
3+
24
@inject NavigationManager NavigationManager
35
46
##endif*@

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Error.razor

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626

2727
@code{
2828
[CascadingParameter]
29-
public HttpContext? HttpContext { get; set; }
29+
private HttpContext? HttpContext { get; set; }
3030

31-
public string? RequestId { get; set; }
32-
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
31+
private string? RequestId { get; set; }
32+
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
3333

3434
protected override void OnInitialized() =>
3535
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;

0 commit comments

Comments
 (0)