C
C#β€’2mo ago
Kyr

Blazor Web Assembly (stand alone) - [Authorize] Attribute not recognising roles straight after login

have a custom AuthenticationStateProvider that adds roles to the ClaimsPrincipal in public override async Task<AuthenticationState> GetAuthenticationStateAsync() As an anonymous user, when I hit a route protected with the [Authorize(Roles="Administrator")] attribute, the app correctly sends me to login and redirects me back to the protected route after login. However, when I log in as a user account that has the "Administrator" role, upon landing back on the protected route it tells me that I don't have permission to access that page (i.e. I'm logged in but don't have the correct role). If I reload the page in the browser, it then recognises that I have the role and lets me in. In my case, I'm adding the authorize attribute to a whole directory using _Imports.razor:
@using Microsoft.AspNetCore.Authorization
@layout AdminLayout
@attribute [Authorize(Roles = "GlobalAdministrator,Administrator")]
@using Microsoft.AspNetCore.Authorization
@layout AdminLayout
@attribute [Authorize(Roles = "GlobalAdministrator,Administrator")]
Not sure what I'm doing wrong.
28 Replies
Crdl
Crdlβ€’2mo ago
Is GetAuthenticationState being called again once you land back on the protected route? I'd log just to make sure there's no weird order of operations bug
Kyr
Kyrβ€’2mo ago
@Crdl I added an info log at the start of GetAuthenticationStateAsync:
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
_logger.LogInformation("GetAuthenticationStateAsync() called");
// ... snip ...
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
_logger.LogInformation("GetAuthenticationStateAsync() called");
// ... snip ...
}
In the browser log...
info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] GetAuthenticationStateAsync() called info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] Authorization failed. These requirements were not met: RolesAuthorizationRequirement:User.IsInRole must be true for one of the following roles: (GlobalAdministrator|Administrator)
I did the above, then restarted the Blazor app. 1. Went to my /admin route in the browser 2. Was correctly redirected to login page on my OIDC server 3. Logged in - was redirected back to /admin Then saw the logs above
Crdl
Crdlβ€’2mo ago
Is the first GetAuthenticationState call actually getting what you expect? Can you log in there basically everything you should have? And see how that differs after you refresh
Kyr
Kyrβ€’2mo ago
Added more logging... It looks like inside the GetAuthenticationStateAsync() call the user.Identity?.IsAuthenticated is returning false on that run.
info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] ### GetAuthenticationStateAsync() called invoke-js.ts:176 info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] ### We are authenticated: False invoke-js.ts:176 info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] ### DONE
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
_logger.LogInformation("### GetAuthenticationStateAsync() called");
var authState = await base.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identities.Any(x => x.AuthenticationType == "TemplateProject-api"))
{
_logger.LogInformation("### Found custom identity - returning early");
return (await Task.FromResult(new AuthenticationState(user)));
}


_logger.LogInformation("### We are authenticated: {Authed}", user.Identity?.IsAuthenticated);

List<Claim> claims = [];
if (user.Identity?.IsAuthenticated ?? false)
{
// snip - fetching `profile` from my API and populating `claims` with my custom roles
}

var state = Task.FromResult(new AuthenticationState(user));
if (claims.Count != 0)
{
_logger.LogInformation("### Adding new claims identity to principal: {Claims}", JsonSerializer.Serialize(claims.Select(x => new {x.Type, x.Value})));
user.AddIdentity(new ClaimsIdentity(claims, "TemplateProject-api"));
state = Task.FromResult(new AuthenticationState(user));
_logger.LogInformation("### NotifyAuthenticationStateChanged");
NotifyAuthenticationStateChanged(state);
}

_logger.LogInformation("### DONE");
return (await state);
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
_logger.LogInformation("### GetAuthenticationStateAsync() called");
var authState = await base.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identities.Any(x => x.AuthenticationType == "TemplateProject-api"))
{
_logger.LogInformation("### Found custom identity - returning early");
return (await Task.FromResult(new AuthenticationState(user)));
}


_logger.LogInformation("### We are authenticated: {Authed}", user.Identity?.IsAuthenticated);

List<Claim> claims = [];
if (user.Identity?.IsAuthenticated ?? false)
{
// snip - fetching `profile` from my API and populating `claims` with my custom roles
}

var state = Task.FromResult(new AuthenticationState(user));
if (claims.Count != 0)
{
_logger.LogInformation("### Adding new claims identity to principal: {Claims}", JsonSerializer.Serialize(claims.Select(x => new {x.Type, x.Value})));
user.AddIdentity(new ClaimsIdentity(claims, "TemplateProject-api"));
state = Task.FromResult(new AuthenticationState(user));
_logger.LogInformation("### NotifyAuthenticationStateChanged");
NotifyAuthenticationStateChanged(state);
}

_logger.LogInformation("### DONE");
return (await state);
}
After hitting reload in the browser:
info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] ### GetAuthenticationStateAsync() called invoke-js.ts:176 info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] ### We are authenticated: True info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] ### Profile: {"Id":"4592defe-abdf-43bd-833d-4dede705b5aa","Email":"test-user-1@test.com","Name":"Administrator","Roles":[{"Id":"01J3Z3F1X6AJMXA8AAM0PSWAN0","Name":"GlobalAdministrator"}],"Permissions":["Role:Create","Role:Read","Role:Update","Role:Delete","Role:ManagePermissions","UserProfile:Create","UserProfile:Read","UserProfile:Update","UserProfile:Delete","UserProfile:ManageRoles","Permission:Read"]} invoke-js.ts:176 info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] ### Adding roles to claims: http://schemas.microsoft.com/ws/2008/06/identity/claims/role: GlobalAdministrator invoke-js.ts:176 info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] ### Adding new claims identity to principal: [{"Type":"http://schemas.microsoft.com/ws/2008/06/identity/claims/role","Value":"GlobalAdministrator"}] invoke-js.ts:176 info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] ### NotifyAuthenticationStateChanged info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] ### DONE
The issue is that user.Identity?.IsAuthenticated is false at the point where the OIDC server redirects us back to the app I just don't get why since we just logged in
Crdl
Crdlβ€’2mo ago
Just a hunch, maybe just double check your middleware is in the right order on the server?
Kyr
Kyrβ€’2mo ago
It’s stand alone web assembly. No server. Any more ideas @Crdl - I'm at a complete loss 😦
Crdl
Crdlβ€’2mo ago
@Kyr can I see the rest of your AuthenticationStateProvider?
Kyr
Kyrβ€’2mo ago
Sure - attached as it's too big for a message
Crdl
Crdlβ€’2mo ago
What's in the base class?
Kyr
Kyrβ€’2mo ago
That's the default .NET class that is being loaded in web assembly when using OIDC authentication @Crdl I'd happily jump in a voice channel and screen-share if that would help resolve this πŸ™‚
Crdl
Crdlβ€’2mo ago
Am currently at work unfortunately πŸ˜‚
Kyr
Kyrβ€’2mo ago
Me too ... trying to get this working as a POC template in an attempt to get Blazor adoption in the business. Already had to abandon the .NET 8 InteractiveAuto style Server + Wasm way of doing things because it just doesn't work with remote auth when you need client components to make authenticated requests to an external API... now having different auth problems with pure wasm. It's so frustrating because I can see Blazor's potential... yet it roadblocks me at every turn
Crdl
Crdlβ€’2mo ago
GitHub
aspnetcore/src/Components/WebAssembly/WebAssembly.Authentication/sr...
ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux. - dotnet/aspnetcore
Kyr
Kyrβ€’2mo ago
Not sure how it can be if I'm overriding GetAuthenticationStateAsync()
Want results from more Discord servers?
Add your server