Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Milad-Rashidi-Git opened this issue Sep 22, 2024 · 18 comments
Open

Comments

@Milad-Rashidi-Git
Copy link

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.
@jchannon
Copy link
Member

I think middleware is your best option tbh

@jchannon
Copy link
Member

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;
    });

@Milad-Rashidi-Git
Copy link
Author

I think middleware is your best option tbh

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

@JoeStead
Copy link
Collaborator

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

@Milad-Rashidi-Git
Copy link
Author

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
Copy link
Author

@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.

@JoeStead
Copy link
Collaborator

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?

@Milad-Rashidi-Git
Copy link
Author

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?

@JoeStead
Copy link
Collaborator

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

@jchannon
Copy link
Member

jchannon commented Sep 23, 2024 via email

@Milad-Rashidi-Git
Copy link
Author

Milad-Rashidi-Git commented Sep 23, 2024

@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

@JoeStead
Copy link
Collaborator

That template generates an MVC project

@Milad-Rashidi-Git
Copy link
Author

Milad-Rashidi-Git commented Sep 23, 2024

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?

@jchannon
Copy link
Member

#339

@Milad-Rashidi-Git
Copy link
Author

@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?

@jchannon
Copy link
Member

jchannon commented Sep 23, 2024 via email

@Milad-Rashidi-Git
Copy link
Author

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.

@MrDave1999
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants