I have a dotnet 7 Blazor WASM app, using Azure AD B2C (via AddMsalAuthentication
in Program.cs
).
The homepage of the app allows anonymous access, and features a call-to-action to login if the user is not authenticated.
In the layout used by the homepage, I have a dropdown that is populated via an API call. This will attempt to make the API call if User.Identity?.IsAuthenticated == true
. The API call uses an HTTP client using BaseAddressAuthorizationMessageHandler
(which in turn inherits AuthorizationMessageHandler
) which is responsible for silently obtaining an access token before making the call.
If obtaining an access token fails, AccessTokenNotAvailableException
is thrown and I call Redirect()
on the exception, which redirects to the B2C login screen. This is not the behaviour I want as users are redirected without responding to the call-to-action to log in.
Delving into the library code, this seems to be where Blazor WASM creates the ClaimsIdentity
. It calls a JS method in MSAL.js, AuthenticationService.getUser
:
If I call the AuthenticationService.getUser
method in my browser, I can see an object returned:
I note the exp
claim for the user is a few days ago - in my mind this has expired.
I also notice ClaimsIdentity.IsAuthenticated
returns true
when the AuthenticationType
is non-null. In my case, the AuthenticationType
is a Guid matching my B2C app's ClientId
.
So my question is: what should I be calling, other than User.Identity?.IsAuthenticated == true
to determine whether the user is authenticated, and an access token can be provisioned without the user having to re-authenticate? Should I be using Blazor custom policies?
CodePudding user response:
After some more investigation, my conclusion is this behaviour is by design.
The default policy out-of-the box simply adds RequireAuthenticatedUser
:
This doesn't necessarily do what you'd expect - it adds DenyAnonymousAuthorizationRequirement
:
Which in turn simply checks that the Identity is authenticated (which as I noted before just checks for a non-null authentication type):
My solution was to use a custom policy - in services.AddAuthorizationCore
- to check the exp
claim:
new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim("email")
.AddAuthenticationSchemes("bearer")
.RequireAssertion(ctx =>
{
var exp = ctx.User.Claims.SingleOrDefault(c => c.Type.Equals("exp" , StringComparison.OrdinalIgnoreCase));
if (exp is null)
return false;
var datetime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(exp.Value));
return datetime.UtcDateTime > Clock.Instance.UtcNow;
});