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
:
Not sure what I'm doing wrong.28 Replies
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
@Crdl I added an info log at the start of
GetAuthenticationStateAsync
:
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
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
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] ### DONEAfter 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":"[email protected]","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] ### DONEThe 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 inJust a hunch, maybe just double check your middleware is in the right order on the server?
ASP.NET Core Middleware
Learn about ASP.NET Core middleware and the request pipeline.
Itβs stand alone web assembly. No server.
Any more ideas @Crdl - I'm at a complete loss π¦
@Kyr can I see the rest of your
AuthenticationStateProvider
?Sure - attached as it's too big for a message
What's in the base class?
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 π
Am currently at work unfortunately π
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
https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs#L104 could it be this
useCache
flag?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
Not sure how it can be if I'm overriding
GetAuthenticationStateAsync()
You're fetching the base one on line 27 of what you sent me
Yeah, I see. So a bug in Blazor then?
Because without my custom provider it would still be doing that
Hmm, not sure if necessarily a bug. We've not used
RemoteAuthenticationService
internally we've just inherited from AuthenticationStateProvider
directlyBut then I don't get how... because using OIDC the user is redirected away from the Blazor app to the OIDC provider to log in, then redirected back to the app after logging in.
Shouldn't that mean that the Blazor app is re-bootstrapped when the user lands back on it?
Which should do the same as if I reload the page. Yet reloading the page causes it to suddenly see the correct authentication
It needs to inherit the
RemoteAuthenticationService
or the OIDC stuff doesn't workI was thinking that
GetAuthenticationStateAsync
could be being called before everything is set up.. but actually if it's just fetching from JS then I have no idea
Worth trying to turn this caching off at any rateBut the cache isn't persisting anywhere
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
It's just a private field
So on re-loading the page either manually or from a redirect back into the app it's a brand new instance of the wasm app so it should be empty the first time through
@Crdl Swapped this:
for this:
Which bypasses the caching in the private
GetUser()
method of the base class.
Same issue π¦
base.GetAuthenticatedUser()
is getting the user directly from JSInterop
Still getting the log...
info: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] ### We are authenticated: False
If you manually call JSRuntime.InvokeAsync(authentication service.getUser) or whatever at that point in the code, what do you get back?
Could be worth putting some breakpoints into devtools https://github.com/dotnet/aspnetcore/blob/3d13fae15873da334bbff2371b6892346a931093/src/Components/WebAssembly/Authentication.Msal/src/Interop/AuthenticationService.ts#L37C11-L37C28
GitHub
aspnetcore/src/Components/WebAssembly/Authentication.Msal/src/Inter...
ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux. - dotnet/aspnetcore
FIXED IT! π₯³
Instead of overriding
GetAuthenticationStateAsync()
I've changed to overriding GetAuthenticatedUser()
instead.Virtually the same code, just in a different method and I just return the ClaimsPrincipal now instead of the AuthenticationState
I have absolutely no idea why this makes a difference ... the default
GetAuthenticationStateAsync()
is just calling the GetAuthenticatedUser()
under the hood anyway so it should produce the same end result... but here we are