Carter icon indicating copy to clipboard operation
Carter copied to clipboard

How to register a global ASP.NET Core response masking filter?

Open Milad-Rashidi-Git opened this issue 1 year ago • 18 comments

I am using Carter (version 8.1.0) and have developed some minimal APIs with CQRS and vertical slice architecture in my .Net Core 8 web application. I need to mask some sensitive data in the response, for which I’ve created a ResultFilterAttribute as shown below:

public class MaskSensitiveDataAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        // Check if the result is an ObjectResult (which holds the JSON response)  
        if (context.Result is ObjectResult objectResult)
        {
            var responseObject = objectResult.Value;
            if (responseObject != null)
            {
                var responseType = responseObject.GetType();
                var jsonResponse = JsonConvert.SerializeObject(responseObject);
                var maskedJsonResponse = MaskResponse(jsonResponse, responseType);

                // Create a new ObjectResult with masked data  
                context.Result = new ObjectResult(JsonConvert.DeserializeObject(maskedJsonResponse))
                {
                    StatusCode = objectResult.StatusCode,
                    ContentTypes = { "application/json" }
                };
            }
        }

        base.OnResultExecuting(context);
    }

    private string MaskResponse(string jsonResponse, Type modelType)
    {
        var jsonObject = JsonConvert.DeserializeObject<JObject>(jsonResponse);
        if (jsonObject != null)
        {
            MaskSensitiveProperties(jsonObject, modelType);
        }

        return jsonObject.ToString(Formatting.None);
    }

    private void MaskSensitiveProperties(JObject jsonObject, Type modelType)
    {
        var properties = modelType.GetProperties();

        foreach (var property in properties)
        {
            if (Attribute.IsDefined(property, typeof(MaskedAttribute)))
            {
                if (jsonObject.ContainsKey(property.Name))
                {
                    jsonObject[property.Name] = new string('*', jsonObject[property.Name].ToString().Length);
                }
            }
        }
    }
}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class MaskedAttribute : Attribute { }

This is the Program.cs file:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
var assembly = typeof(Program).Assembly;

builder.Services
    .AddCarter()
    .AddSwaggerAndUI()
    .AddApplicationServices()
    .ConfigureCrossOriginPolicy()
    .RegisterMappingsAndTypeConfigs()
    .AddMediatorConfiguration(assembly)
    .AddValidatorsFromAssembly(assembly)
    .AddAuthConfig(builder.Configuration)
    .AddApplicationSettings(builder.Configuration)
    .AddEfConfig(builder.Configuration, assembly.FullName, true)
    .AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

WebApplication app = builder.Build();
IdentityModelEventSource.ShowPII = true;

await app
    .UseMiddleware<ExceptionHandlingMiddleware>()
    .UseSwaggerAndUI(app)
    .UseCors("AllowAllOrigins")
    .UseAuthentication()
    .UseAuthorization()
    .UseAntiforgery()
    .ApplyDbMigrationsAsync(app);

app.MapCarter();

await app.RunAsync();

To add the MaskSensitiveDataAttribute filter to the pipeline, I researched extensively and expected to find something like the following code:

services.AddCarter(null, config =>  
{
    config.Filters.Add<MaskSensitiveDataAttribute>(); // Here I wanted to register global response masking filter
    config.AddFilter<MaskSensitiveDataAttribute>(); // Or register it like this
});

However, the AddCarter extension method is defined as follows and takes an Action<CarterConfigurator>, which doesn’t provide an option to add filters:

public static IServiceCollection AddCarter(this IServiceCollection services,
    DependencyContextAssemblyCatalog assemblyCatalog = null,
    Action<CarterConfigurator> configurator = null)

My question is: How can I register this global response masking filter in carter?

If there were a solution to not using reflection, it would be much appreciated as the reflection has a performance overhead.

Please Note: I considered creating a response masking middleware, but it has some issues:

  1. I don't know the type of the response model to find all properties decorated with the Masked attribute. (While it’s possible to set the type of the response DTO using HttpContext.Items["ResponseModelType"] and read it in the middleware, I prefer not to do this as it requires passing this data in all endpoints.)
  2. It necessitates maintaining a predefined list of property names that must be masked.
  3. It requires looking up in the response body and masking all properties with similar names, such as Email, but there may be cases where we do not want to mask Email.

Milad-Rashidi-Git avatar Sep 22 '24 20:09 Milad-Rashidi-Git

I think middleware is your best option tbh

jchannon avatar Sep 23 '24 15:09 jchannon

Or you use some sort of service class that you inject into the routes you know the types you want to mask

app.MapGet("/foo/{id}", string id, IDb db, IMaskService maskService) =>
    {
        var entity = db.GetById(id);

       var maskedEntity = maskService.Mask(entity);
        return maskedEntity;
    });

jchannon avatar Sep 23 '24 15:09 jchannon

I think middleware is your best option tbh

I don't think so because of those reasons I mentioned in the question.

Milad-Rashidi-Git avatar Sep 23 '24 15:09 Milad-Rashidi-Git

Which specific reason? I don't see why this can't be achieved by middleware with the information presented

JoeStead avatar Sep 23 '24 15:09 JoeStead

Or you use some sort of service class that you inject into the routes you know the types you want to mask

app.MapGet("/foo/{id}", string id, IDb db, IMaskService maskService) =>
    {
        var entity = db.GetById(id);

       var maskedEntity = maskService.Mask(entity);
        return maskedEntity;
    });

As I mentioned in my question, I need to automate the Data Masking process.

Milad-Rashidi-Git avatar Sep 23 '24 15:09 Milad-Rashidi-Git

@JoeStead I considered creating a response masking middleware, but it has some issues:

  1. I don't know the type of the response model to find all properties decorated with the Masked attribute. (While it’s possible to set the type of the response DTO using HttpContext.Items["ResponseModelType"] and read it in the middleware, I prefer not to do this as it requires passing this data in all endpoints.)
  2. It necessitates maintaining a predefined list of property names that must be masked.
  3. It requires looking up in the response body and masking all properties with similar names, such as Email, but there may be cases where we do not want to mask Email.

Milad-Rashidi-Git avatar Sep 23 '24 16:09 Milad-Rashidi-Git

Carter operates at the level you're having issues with. The issues you're facing, carter will have to deal with internally too.

If you want to mask responses, surely the correct solution would be to not send fields, and do any masking on the UI?

JoeStead avatar Sep 23 '24 16:09 JoeStead

Carter operates at the level you're having issues with. The issues you're facing, carter will have to deal with internally too.

If you want to mask responses, surely the correct solution would be to not send fields, and do any masking on the UI?

Are you saying that there is no way to add an ASP.NET Core result filter to Carter?

Milad-Rashidi-Git avatar Sep 23 '24 16:09 Milad-Rashidi-Git

That is an ASP.NET Core MVC feature, not asn ASP.NET Core feature afaik

JoeStead avatar Sep 23 '24 16:09 JoeStead

You can use endpoint filter. Not sure about result filter

On Mon, 23 Sep 2024 at 17:07, Milad Rashidi @.***> wrote:

Carter operates at the level you're having issues with. The issues you're facing, carter will have to deal with internally too.

If you want to mask responses, surely the correct solution would be to not send fields, and do any masking on the UI?

Are you saying that there is no way to add an ASP.NET Core result filter to Carter?

— Reply to this email directly, view it on GitHub https://github.com/CarterCommunity/Carter/issues/363#issuecomment-2368740143, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAZVJXL5JNFHVD22W2KV4TZYA4E7AVCNFSM6AAAAABOU3O6ESVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGNRYG42DAMJUGM . You are receiving this because you commented.Message ID: @.***>

jchannon avatar Sep 23 '24 16:09 jchannon

@JoeStead Since ASP.NET Core integrates MVC into its structure, even in an API project, you can still leverage filters, including result filters.

dotnet new webapi -n SampleApiWithFilter  
cd SampleApiWithFilter
// Filters/MyResultFilter.cs  
using Microsoft.AspNetCore.Mvc.Filters;  
using Microsoft.AspNetCore.Mvc;  
using System.Threading.Tasks;  

public class MyResultFilter : IAsyncResultFilter  
{  
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)  
    {  
        // Code to execute before the result is processed  
        // For example, adding a custom header  
        context.HttpContext.Response.Headers.Add("X-My-Custom-Header", "HeaderValue");  

        // Continue executing the next result filter or the action result  
        await next(); // Call the next delegate/middleware in the pipeline  

        // Code to execute after the result is processed  
        // You can log results or modify the response if needed  
    }  
}
// Controllers/WeatherForecastController.cs  
using Microsoft.AspNetCore.Mvc;  
using System.Collections.Generic;  

[ApiController]  
[Route("[controller]")]  
[ServiceFilter(typeof(MyResultFilter))] // Apply filter at the controller level  
public class WeatherForecastController : ControllerBase  
{  
    private static readonly string[] Summaries = new[]  
    {  
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"  
    };  

    [HttpGet]  
    public IEnumerable<WeatherForecast> Get()  
    {  
        var rng = new Random();  
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast  
        {  
            Date = DateTime.Now.AddDays(index),  
            TemperatureC = rng.Next(-20, 55),  
            Summary = Summaries[rng.Next(Summaries.Length)]  
        })  
        .ToArray();  
    }  
}  

public class WeatherForecast  
{  
    public DateTime Date { get; set; }  
    public int TemperatureC { get; set; }  
    public string Summary { get; set; }  
}
public class Startup  
{  
    public void ConfigureServices(IServiceCollection services)  
    {  
        services.AddControllers(options =>  
        {  
            // You can also apply globally with options.Filters.Add(new MyResultFilter());  
        });  
        services.AddScoped<MyResultFilter>(); // Registering the filter  
    }  

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
    {  
        if (env.IsDevelopment())  
        {  
            app.UseDeveloperExceptionPage();  
        }  

        app.UseRouting();  
        
        app.UseAuthorization();  

        app.UseEndpoints(endpoints =>  
        {  
            endpoints.MapControllers();  
        });  
    }  
}
dotnet run

Milad-Rashidi-Git avatar Sep 23 '24 16:09 Milad-Rashidi-Git

That template generates an MVC project

JoeStead avatar Sep 23 '24 16:09 JoeStead

You can use endpoint filter. Not sure about result filter

I'm okay with using an Endpoint Filter instead of a Result Filter. How to add it to the carter?

Milad-Rashidi-Git avatar Sep 23 '24 16:09 Milad-Rashidi-Git

https://github.com/CarterCommunity/Carter/issues/339

jchannon avatar Sep 23 '24 16:09 jchannon

@jchannon

I believe that the issues I mentioned regarding response middleware also apply to Endpoint Filters. How can I determine the type of the response DTO model within an IEndpointFilter? For instance, how can I ascertain whether this response is of type UserDto?

Milad-Rashidi-Git avatar Sep 23 '24 17:09 Milad-Rashidi-Git

No idea I’m afraid

You could look to use Carters IResponseNegotiator will give you object you can do a Gettype on but not sure this is the right thing to do either

On Mon, 23 Sep 2024 at 18:10, Milad Rashidi @.***> wrote:

@jchannon https://github.com/jchannon

I believe that the issues I mentioned regarding response middleware also apply to Endpoint Filters. How can I determine the type of the response DTO model within an IEndpointFilter? For instance, how can I ascertain whether this response is of type UserDto ?

— Reply to this email directly, view it on GitHub https://github.com/CarterCommunity/Carter/issues/363#issuecomment-2368880644, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAZVJRA37X2GGL26P7KTSTZYBDOTAVCNFSM6AAAAABOU3O6ESVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGNRYHA4DANRUGQ . You are receiving this because you were mentioned.Message ID: @.***>

jchannon avatar Sep 23 '24 17:09 jchannon

No idea I’m afraid You could look to use Carters IResponseNegotiator will give you object you can do a Gettype on but not sure this is the right thing to do either

Ok, thanks.

Milad-Rashidi-Git avatar Sep 23 '24 17:09 Milad-Rashidi-Git

@Milad-Rashidi-Git The solution is simple: Don't use minimal APIs. Use controllers.

DevD4v3 avatar Sep 25 '24 00:09 DevD4v3