0

We have a Blazor Server app that requires authorization on all its components (internal business app):

// ...

app.UseHttpsRedirection();
app.MapStaticAssets();  // <-- Static files middleware before auth.
app.UseAuthentication();
app.UseAuthorization();

// ...

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .RequireAuthorization(); // <-- All pages require auth.

We also have a component in a Razor Class Library (RCL) that requires JavaScript to work. We've put it inside a .razor.js collocated file.

We are using a mix of Azure AD and Azure B2C in our auth flow (handled via OIDC), and our Routes.razor uses the <AuthorizeRouteView> component to render our pages/layouts.

Everything seemed to work fine, but we've hit an issue when the user's Blazor Server connection times out (i.e. they leave the app running in the background for 15-30 minutes).

We get this in the browser's logs (sensitive information redacted):

Access to script at 'https://login.microsoftonline.com/{OIDC URL parameters redacted}' (redirected from 'https://{site URL}/_content/{RCL}/Components/MyComponent.razor.js') from origin 'https://{site URL}' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Failed to load resource: net::ERR_FAILED

Error: Microsoft.JSInterop.Exception: Failed to fetch dynamically imported module: 'https://{site URL}/_content/{RCL}/Components/MyComponent.razor.js'

{Stack trace that shows it tried and failed to import module}

Information: Connection disconnected.

My understanding is that Blazor tries to reconnect, but it tries to access the collocated JavaScript file (which is apparently a protected resources despite our static assets middleware being configured before the authentication/authorization middleware) before actually renewing the auth.

This crashes (unhandled exception, Blazor's error ui pops-up) because it causes a redirect to our Azure AD/B2C setup, but that doesn't know how to handle our static assets, because they're not supposed to be protected resources (and we don't want to have to configure every single static asset in Azure).

This leads me to believe that maybe collocated JavaScript files are considered "a part of" their component as far as .RequireAuthorization() is concerned?

Am I missing something? I can't find anything in the documentation about collocated JavaScript files behaving differently with the authentication. In fact, the docs state this (Emphasis: mine):

[...] Blazor takes care of placing the JS file in published static assets for you.

Once the user refreshes the page, everything works as expected. So maybe it's something to do with how renewing a Blazor connection works? I'm honestly not sure why this is happening. Any help would be appreciated.


Edit: I've found that I am able to consistently reproduce the problem by clearing my cookies and running a GET request on the JavaScript file directly without logging in again. Since this works outside of a Blazor context, I'm thinking that the problem probably due to how Blazor adds its collocated files to the static assets.

8
  • looks like it thinks that .js file is a component. The CORS error comes from the browser. It's trying to perform a pre-flight because there's a non-simple request (probably from javascript) to that .js file. (a fetch or something like that...) Since all requests to endpoints/components require authentication when the session has timed-out you're being redirected to login when trying to fetch the .js file. (at that point it's cross-domain and CORS is triggered) I don't really understand why the Access-Control-Allow-Origin headers are missing from login.microsoft though. Commented Jul 16 at 17:28
  • If it helps, the .razor.js file is used to do things like input masking and CSS changes on the component (it's a wrapper around a third-party library). It doesn't do anything asynchronously or fetches on its own (unless I misunderstood and you meant that the Blazor JS was doing those). Commented Jul 16 at 17:33
  • I think blazor is dynamically importing it using a fetch call. (a sort of module on demand) The problem is it's redirecting that request because the session has idle timed out. The redirect sort of falls through the same fetch causing the CORS request. For a normal page load that would just go to the browser and origin would be null (so no CORS error), but it originated via JS. (You can change the idle time-out btw... I think by default it's 20 minutes) If you can add an allowed origin to login.microsoft, do that. Otherwise, you need to set location.href to MS's login page on error? Commented Jul 16 at 17:40
  • OR, set the .js file/component (endpoint?) so that it allows anonymous. Not sure how you'd do that with an asset though. (those should already allow anonymous methinks) Usually you'd set that for an endpoint/controller. Maybe using route mapping. Commented Jul 16 at 17:44
  • I know UseStaticFiles() can be configured with routing (and there's docs out there for how to handle auth with them), but I'm not sure how that'd work with Blazor's collocated JavaScript files. I'll try looking into that, just in case. Commented Jul 16 at 17:54

1 Answer 1

1

As of writing this (2025-01-17 using .NET 9), Microsoft's documentation regarding MapStaticAssets() being a drop in replacement for UseStaticFiles() appears to be incorrect. This may change in the future.

If you want MapStaticAssets() to make all static assets available without auth, you may need to add .AllowAnonymous().

I say "may", because my problem was caused by the fact that my authorization registration was done like this:

services.AddAuthorizationBuilder()
    .SetFallbackPolicy(new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build());

The above works fine with UseStaticFiles(), but not with MapStaticAssets() (ergo, it's not actually a drop in replacement).


It's possible that this whole thing is due to a bug.

I can confirm that the entire pipeline runs when using MapStaticAssets(). I added the following middleware to test it out:

app.MapStaticAssets().AllowAnonymous();
// or
app.UseStaticFiles();

app.Use(async (context, next) =>
{
    var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
    logger.LogWarning("BEFORE AUTH HIT");
    await next.Invoke();
});
app.UseAuthentication();
app.UseAuthorization();
app.Use(async (context, next) =>
{
    var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
    logger.LogWarning("AFTER AUTH HIT");
    await next.Invoke();
});

With MapStaticAssets(), the BEFORE AUTH HIT and AFTER AUTH HIT messages are both logged. This does not happen with UseStaticFiles().


For now, my two options seem to be:

  • Use MapStaticAssets().AllowAnonymous():
    • This provides the benefits of MapStaticAssets(), and allows unauthenticated users to get static assets.
    • Unfortunately, it runs a large part of the pipeline that isn't needed for these, so there's a performance hit.
  • Use UseStaticFiles():
    • I lose out on the benefits of MapStaticAssets() (i.e. a better browser cache experience).
    • The pipeline shortcircuits properly, so there's no performance hit.
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.