dotnet WebApi + Client - ClientApp SPA is not protected by .AddAuthentication or .AddOAuth

I have a standard dotnet WebApi + Client application with the ClientApp folder containing a create-react-app SPA

In my Startup - I have .AddAuthentication

.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = "MyChallenge"
})
  .AddCookie()
  .AddOAuth("MyChallenge", oauthAction);

and my oAuthAction is

// https://auth0.com/docs/get-started/authentication-and-authorization-flow/add-login-auth-code-flow
Action<OAuthOptions> oauthAction = (OAuthOptions options) =>
{
    var domainBase = "https://login.mydomain.com";

    // When a user needs to sign in, they will be redirected to the authorize endpoint
    options.AuthorizationEndpoint = $"{domainBase}/authorize";

    // OAuth server is OpenID compliant, so request the standard openid
    // scopes when redirecting to the authorization endpoint
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");

    // After the user signs in, an authorization code will be sent to a callback
    // in this app. The OAuth middleware will intercept it
    options.CallbackPath = new PathString("/auth/callback");

    // The OAuth middleware will send the ClientId, ClientSecret, and the
    // authorization code to the token endpoint, and get an access token in return
    options.ClientId = "NONEOFYOURBUSINESS";
    options.ClientSecret = "NICETRYBUTNOT";
    options.TokenEndpoint = $"{domainBase}/token";

    // Below we call the userinfo endpoint to get information about the user
    options.UserInformationEndpoint = $"{domainBase}/userinfo";

    // Describe how to map the user info we receive to user claims
    options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
    options.ClaimActions.MapJsonKey(ClaimTypes.Name, "given_name");
    options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");

    options.Events = new OAuthEvents
    {
        OnCreatingTicket = async context =>
        {
            // Get user info from the userinfo endpoint and use it to populate user claims
            var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);

            var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
            response.EnsureSuccessStatusCode();

            var responseContent = await response.Content.ReadAsStringAsync();
            var user = JsonSerializer.Deserialize<JsonElement>(responseContent);
            context.RunClaimActions(user);
        }
    };
};

While every controller marked by [Authorize] is protected, my SPA static files are not. For "reasons" (that are not relevant) I need them to be.

I've looked around but I can't seem to find a way to protect the SPA files defined by

builder.Services.AddSpaStaticFiles(
    configuration =>
    {
        configuration.RootPath = "ClientApp/build";
    }
);

Or

app.UseSpa(
    spa =>
    {
        spa.Options.SourcePath = "ClientApp";
        if (app.Environment.IsDevelopment())
        {
            spa.UseReactDevelopmentServer(npmScript: "start");
            spa.UseProxyToSpaDevelopmentServer("http://127.0.0.1:3000");
        }
    }
);

to require the [Authorize] middleware - it seems like it should be simple - what am I missing? One would think this would be a simple / semi-common scenario.


Solution 1:

C# CODE

There is an OnPrepareResponse hook that you can use for this. The link provides a nice example.

WEB RESOURCES

Ir's pretty rare to secure static content, in either an SPA or a website, since most apps use at least one unauthenticated view, eg to present a login button, before the user authenticates. This in turn requires JS, CSS etc to be downloaded when there is no auth cookie yet.

I would potentially split static files into folders - those that need authorizing in one root folder, and those that don't in another. The files that need authorizing may need to run more stages of the ASP.Net pipeline, eg to process cookies.