Protect Static Files with Authentication on ASP.NET Core

Viewed 2558

In my previous question, I asked a generic question how to add permission for static content. Here I want to be more precise.

In my project I added a folder under wwwroot to simplify the code, where I save the html files I want to protect. This folder is called infographics.

enter image description here

The properties for each file are:

  • Build Action: Content
  • Copy to Output Directory: Do not copy

enter image description here

Follow the instruction from the Microsoft documentation, I changed the Startup.cs

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.Configure<IdentityServerConfiguration>(Configuration.GetSection("IdentityServerConfiguration"));

        services.AddDistributedMemoryCache();

        services.AddSession(options =>
        {
            options.Cookie.Name = ".my.Session";
            options.IdleTimeout = TimeSpan.FromHours(12);
        });

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = "oidc";
        })
        .AddCookie(options =>
        {
            options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
            options.Cookie.Name = "my.dashboard";
        })
        .AddOpenIdConnect("oidc", options =>
        {
            IdentityServerConfiguration idsrv = Configuration.GetSection("IdentityServerConfiguration")
                                                .Get<IdentityServerConfiguration>();
            options.Authority = idsrv.Url;
            options.ClientId = idsrv.ClientId;
            options.ClientSecret = idsrv.ClientSecret;

            #if DEBUG
            options.RequireHttpsMetadata = false;
            #else
            options.RequireHttpsMetadata = true;
            #endif

            options.ResponseType = "code";

            options.Scope.Clear();
            options.Scope.Add("openid");
            options.Scope.Add("profile");
            options.Scope.Add("email");
            options.Scope.Add("roles");
            options.Scope.Add("offline_access");

            options.ClaimActions.MapJsonKey("role", "role", "role");

            options.GetClaimsFromUserInfoEndpoint = true;
            options.SaveTokens = true;

            options.SignedOutRedirectUri = "/";

            options.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = JwtClaimTypes.Name,
                RoleClaimType = JwtClaimTypes.Role,
            };
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        app.UseHttpsRedirection();

        app.UseAuthentication();

        app.UseStaticFiles(new StaticFileOptions
        {
            OnPrepareResponse = ctx =>
            {
                if (ctx.Context.Request.Path.StartsWithSegments("/infographics"))
                {
                    ctx.Context.Response.Headers.Add("Cache-Control", "no-store");

                    if (!ctx.Context.User.Identity.IsAuthenticated)
                    {
                        // respond HTTP 401 Unauthorized with empty body.
                        ctx.Context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                        ctx.Context.Response.ContentLength = 0;
                        ctx.Context.Response.Body = Stream.Null;

                        // - or, redirect to another page. -
                        // ctx.Context.Response.Redirect("/");
                    }
                }
            }
        });

        app.UseRouting();

        app.UseAuthorization();

        app.UseCookiePolicy();
        app.UseSession();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

What I expect is when the user asks for /infographics the OnPrepareResponse verifies the request and if the user is authenticated sees the page. But, after a lot of code changing, the result is always the same (on my local machine):

This localhost page can’t be found

I tried to add this code to map the folder html in the root of the project as infographics but without success.

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(env.ContentRootPath, "html")),
    RequestPath = "/infographics"
});

Any ideas?

Update

This is working with not HTML files. I think the problem comes from the HTML file because they are static content for ASP.NET.

I put a breakpoint on the OnPrepareResponse and call the page infographics/index.html. The page is displayed (red arrow) and then the application stops on the breakpoint (blue arrow).

enter image description here

2 Answers

For any one else that ends up here searching for protecting secure static files.

For .NET Core 5. Documentation has been updated to include how to handle this

Per the documentation:

To serve static files based on authorization:

  1. Store them outside of wwwroot.
  2. Call UseStaticFiles, specifying a path, after calling UseAuthorization.
  3. Set the fallback authorization policy.

Make sure to add a fallback policy as well:

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

See: Static file authorization

I would recommend to place that folder outside the wwwroot folder, which will remove the static file access to it.

You can then serve your files from a controller like so:

public IActionResult GetInfographic(string name)
{
    var infographicPath = resolvePath(name);
    return new FileStreamResult(new FileStream(infographicPath, FileMode.Open, FileAccess.Read), mime);
}

given this is a controller you can use the [Authorize] tag to secure it any way you'd like.

Related