Question

I detected a problem in the RequestFilter execution order.

The ValidationFeature in ServiceStack is a Plugin that just registers a Global Request Filter. The Order of Operations points out that Global Request Filters are executed after Filter Attributes with a Priority <0 and before Filter Attributes with a Priority >=0

My BasicAuth filter has -100 priority, and in fact everything goes well if the Service is annotated at class level, but it fails when the annotation is at method level, with the authentication filter being executed after.

I am using 3.9.70 Is there any quick fix for this? Thanks

Was it helpful?

Solution

When you add the annotation at method level then you are creating an Action Request Filter (because you are adding the annotation to an action method) which in the Order of Operations is operation 8, after the other filters have run.

5: Request Filter Attributes with Priority < 0 gets executed
6: Then any Global Request Filters get executed
7: Followed by Request Filter Attributes with Priority >= 0
8: Action Request Filters (New API only)


The best workaround I can suggest is to reconsider your service structure. I imagine you are having these difficulties because you are adding unauthenticated api methods alongside your secure api methods, and thus are using method level attributes to control authentication. So you are presumably doing something like this Your classes and attributes will be different, this is just exemplar:

public class MyService : Service
{
    // Unauthenticated API method
    public object Get(GetPublicData request)
    {
        return {};
    }

    // Secure API method
    [MyBasicAuth] // <- Checks user has permission to run this method
    public object Get(GetSecureData request)
    {
        return {};
    }
}

I would do this differently, and separate your insecure and secure methods into 2 services. So I use this:

// Wrap in an outer class, then you can still register AppHost with `typeof(MyService).Assembly`
public partial class MyService
{
    public class MyPublicService : Service
    {
        public object Get(GetPublicData request)
        {
            return {};
        }
    }

    [MyBasicAuth] // <- Check is now class level, can run as expected before Validation
    public class MySecureService : Service
    {
        public object Get(GetSecureData request)
        {
            return {};
        }
    }
}

Solution - Deferred Validation:

You can solve your execution order problem by creating your own custom validation feature, which will allow you to defer the validation process. I have created a fully functional self hosted ServiceStack v3 application that demonstrates this.

Full source code here.

Essentially instead of adding the standard ValidationFeature plugin we implement a slightly modified version:

public class MyValidationFeature : IPlugin
{
    static readonly ILog Log = LogManager.GetLogger(typeof(MyValidationFeature));
    
    public void Register(IAppHost appHost)
    {
        // Registers to use your custom validation filter instead of the standard one.
        if(!appHost.RequestFilters.Contains(MyValidationFilters.RequestFilter))
            appHost.RequestFilters.Add(MyValidationFilters.RequestFilter);
    }
}

public static class MyValidationFilters
{
    public static void RequestFilter(IHttpRequest req, IHttpResponse res, object requestDto)
    {
        // Determine if the Request DTO type has a MyRoleAttribute.
        // If it does not, run the validation normally. Otherwise defer doing that, it will happen after MyRoleAttribute.
        if(!requestDto.GetType().HasAttribute<MyRoleAttribute>()){
            Console.WriteLine("Running Validation");
            ValidationFilters.RequestFilter(req, res, requestDto);
            return;
        }

        Console.WriteLine("Deferring Validation until Roles are checked");
    }
}

Configure to use our plugin:

// Configure to use our custom Validation Feature (MyValidationFeature)
Plugins.Add(new MyValidationFeature());

Then we need to create our custom attribute. Your attribute will be different of course. The key thing you need to do is call ValidationFilters.RequestFilter(req, res, requestDto); if you are satisfied the user has the required role and meets your conditions.

public class MyRoleAttribute : RequestFilterAttribute
{
    readonly string[] _roles;

    public MyRoleAttribute(params string[] roles)
    {
        _roles = roles;
    }

    #region implemented abstract members of RequestFilterAttribute

    public override void Execute(IHttpRequest req, IHttpResponse res, object requestDto)
    {
        Console.WriteLine("Checking for required role");

        // Replace with your actual role checking code
        var role = req.GetParam("role");
        if(role == null || !_roles.Contains(role))
            throw HttpError.Unauthorized("You don't have the correct role");

        Console.WriteLine("Has required role");

        // Perform the deferred validation
        Console.WriteLine("Running Validation");
        ValidationFilters.RequestFilter(req, res, requestDto);
    }

    #endregion
}

For this to work we need to apply our custom attribute on the DTO route not the action method. So this will be slightly different to how you are doing it now, but should still be flexible.

[Route("/HaveChristmas", "GET")]
[MyRole("Santa","Rudolph","MrsClaus")] // Notice our custom MyRole attribute.
public class HaveChristmasRequest {}

[Route("/EasterEgg", "GET")]
[MyRole("Easterbunny")]
public class GetEasterEggRequest {}

[Route("/EinsteinsBirthday", "GET")]
public class EinsteinsBirthdayRequest {}

Then your service would look something like this:

public class TestController : Service
{
    // Roles: Santa, Rudolph, MrsClaus
    public object Get(HaveChristmasRequest request)
    {
        return new { Presents = "Toy Car, Teddy Bear, Xbox"  };
    }

    // Roles: Easterbunny
    public object Get(GetEasterEggRequest request)
    {
        return new { EasterEgg = "Chocolate" };
    }

    // No roles required
    public object Get(EinsteinsBirthdayRequest request)
    {
        return new { Birthdate = new DateTime(1879, 3, 14)  };
    }
}
  • So when we call the route /EinsteinsBirthday which does not have a MyRole attribute the validation will be called normally, as if using the standard ValidationFeature.

  • If we call the route /HaveChristmas?role=Santa then our validation plugin will determine that the DTO has our attribute and not run. Then our attribute filter triggers and it will trigger the validation to run. Thus the order is correct.

Screenshot

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top