ASP.NET Core  

Serving Files Securely with Authorization in ASP.NET Core

When building modern ASP.NET Core applications, it’s common to serve files such as documents, PDFs, images, or reports to authenticated users. However, if files are stored in a public wwwroot folder or exposed directly through static file middleware, they can be accessed by anyone who knows the URL, even without proper authorization.

This can lead to data leaks, information disclosure, and compliance issues. To prevent this, we must implement a secure file-serving mechanism with authorization checks.

Why Not Serve Sensitive Files From?

The wwwroot folder is meant for public static assets (like CSS, JS, and images). Anything inside it is directly accessible via URL.

For sensitive files (e.g., invoices, medical records, private reports):

  • Place them outside wwwroot.

  • Serve them via controller endpoints with authentication & authorization.

Step 1: Project Setup

Create a new project (if not already):

dotnet new webapp -n SecureFileDemo

Enable authentication (if required) or use custom policies/roles.

Step 2: Store Files Outside wwwroot

Suppose we store private files inside SecureFiles at the root of the project:

/SecureFiles
   /Reports
      report1.pdf
      report2.pdf

These files are not directly accessible via the browser.

Step 3: Create a Secure File Controller

We’ll build an endpoint that:

  1. Validates the user identity.

  2. Checks authorization (roles, claims, policies).

  3. Serves the file securely.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Threading.Tasks;

namespace SecureFileDemo.Controllers
{
    [Authorize] // ensures only logged-in users
    public class FilesController : Controller
    {
        private readonly string _fileStoragePath = Path.Combine(Directory.GetCurrentDirectory(), "SecureFiles");

        [Authorize(Roles = "Admin,Manager")] // Example: restrict further
        [HttpGet("files/{fileName}")]
        public async Task<IActionResult> GetFile(string fileName)
        {
            // Prevent path traversal attack
            var safeFileName = Path.GetFileName(fileName);
            var filePath = Path.Combine(_fileStoragePath, safeFileName);

            if (!System.IO.File.Exists(filePath))
            {
                return NotFound("File not found.");
            }

            var memory = new MemoryStream();
            using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                await stream.CopyToAsync(memory);
            }
            memory.Position = 0;

            var contentType = "application/octet-stream"; // default
            return File(memory, contentType, safeFileName);
        }
    }
}

Step 4: Fine-Grained Authorization (Per-User Files)

If each user should only access their own files (e.g., invoices), we can enforce checks:

[HttpGet("userfiles/{fileName}")]
public async Task<IActionResult> GetUserFile(string fileName)
{
    var safeFileName = Path.GetFileName(fileName);
    var userId = User.Identity.Name; // Or a claim like User.FindFirst("UserId")

    var userFolder = Path.Combine(_fileStoragePath, userId);
    var filePath = Path.Combine(userFolder, safeFileName);

    if (!System.IO.File.Exists(filePath))
    {
        return NotFound("File not found.");
    }

    // Only serve if it belongs to this user
    if (!filePath.StartsWith(userFolder))
    {
        return Forbid();
    }

    var memory = new MemoryStream();
    using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
    {
        await stream.CopyToAsync(memory);
    }
    memory.Position = 0;

    return File(memory, "application/octet-stream", safeFileName);
}

Step 5: Using IAuthorizationService for Policies

You can implement custom policies for advanced checks (e.g., document ownership):

private readonly IAuthorizationService _authorizationService;

public FilesController(IAuthorizationService authorizationService)
{
    _authorizationService = authorizationService;
}

[HttpGet("secure-docs/{fileName}")]
public async Task<IActionResult> GetSecureDoc(string fileName)
{
    var requirement = new DocumentAccessRequirement(fileName);
    var result = await _authorizationService.AuthorizeAsync(User, null, requirement);

    if (!result.Succeeded)
        return Forbid();

    // Serve the file (same as above)
}

Best Practices

  • Always store sensitive files outside wwwroot.

  • Used Path.GetFileName() to prevent path traversal attacks.

  • Implement role/claim/policy-based authorization.

  • Set appropriate Content-Type headers.

  • Consider streaming large files instead of loading them into memory.

  • Enable logging for file access attempts.

Conclusion

By serving files through a controller with proper authentication and authorization, you can ensure that only authorized users can access sensitive content. This approach is much safer than relying on static file hosting for private documents.

With these patterns, you can securely deliver files in ASP.NET Core while maintaining compliance and protecting user data.