Search code examples
c#asp.net-web-api2owinowin-middleware

Change OWIN Request.Body in middleware


I want to implement custom encryption middleware for API calls. At first, I read request body (IOwinContext.Request.Body) and headers (Encryption-Key & Signature). Then, I decrypt request body, which gives me pure json string. Now comes the tricky part: I want to write this json back to IOwinContextRequest.Body, so it can be deserialized to object and later passed as an argument for Controller method. Here's what I do:

Startup:

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Use(typeof(EncryptionMiddleware));

        ...
    }
}

Middleware:

public class EncryptionMiddleware : OwinMiddleware
{
    public EncryptionMiddleware(OwinMiddleware next) : base(next)
    {
        //
    }

    public async override Task Invoke(IOwinContext context)
    {
        var request = context.Request;
        string json = GetDecryptedJson(context);
        MemoryStream stream = new MemoryStream();
        stream.Write(json, 0, json.Length);
        request.Headers["Content-Lenght"] = json.Lenght.ToString();
        request.Body = stream;
        await Next.Invoke(context);
    }
}

Now, what I get is this error:

System.Web.Extensions.dll!System.Web.Script.Serialization.JavaScriptObjectDeserializer.DeserializePrimitiveObject() Exception thrown: 'System.ArgumentException' in System.Web.Extensions.dll

Additional information: Invalid JSON primitive: 8yi9OH2JE0H0cwZ.

Where original IOwinContext.Request.Body is:

8yi9OH2JE0H0cwZ/fyY5Fks4nW(...omitted...)PvL32AVRjLA==

So I assumed that you cannot change request body this way. To test this, I've rewritten middleware like this:

public async override Task Invoke(IOwinContext context)
{
    var request = context.Request;

    string requestBody = new StreamReader(request.Body).ReadToEnd();
    Debug.WriteLine(requestBody); // Prints "ORIGINAL BODY"
    string newBody = "\"newBody\"";
    MemoryStream memStream = new MemoryStream(Encoding.UTF8.GetBytes(newBody));
    request.Headers["Content-Length"] = newBody.Length.ToString();
    request.Body = memStream;
    await Next.Invoke(context);
}

Now I thought that Controller method should receive "ORIGINAL BODY" instead of "newBody", but I actually got this error:

System.dll!System.Diagnostics.PerformanceCounter.InitializeImpl() Exception thrown: 'System.InvalidOperationException' in System.dll

Additional information: The requested Performance Counter is not a custom counter, it has to be initialized as ReadOnly.

The question is: what is wrong with my approach? What is the correct way to rewrite request body? Is there any sufficient workaround? BTW: Decryption of data is tested and is flawless, so error should not originate there.

EDIT: before you answer/comment, TLS is already used. This is another layer of security. I am NOT reinventing the wheel. I'm adding a new one.


Solution

  • I have solved this issue a long time ago, but only after @Nkosi 's advice, I'm posting the solution.

    What I did is a workaround, or rather "bridge" from middleware to action filter. Here's the code:

    Middleware

    public class EncryptionMiddleware : OwinMiddleware
    {
    
        public EncryptionMiddleware(OwinMiddleware next) : base(next)
        {
            //
        }
    
        public async override Task Invoke(IOwinContext context)
        {
            var request = context.Request;
    
            string requestBody = new StreamReader(request.Body).ReadToEnd();
    
            var obj = // do your work here
            System.Web.HttpContext.Current.Items[OBJECT_ITEM_KEY] = obj;
            await Next.Invoke(context);
            return;
        }
    }
    

    Filter

    public class EncryptedParameter : ActionFilterAttribute, IActionFilter
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var obj = HttpContext.Current.Items[OBJECT_ITEM_KEY];
            HttpContext.Current.Items.Remove(AppConfig.ITEM_DATA_KEY);
    
            if (filterContext.ActionParameters.ContainsKey("data"))
                filterContext.ActionParameters["data"] = obj;
        }
    }
    

    Controller

    public class MyController : Controller
    {
        [HttpPost]
        [EncryptedParameter]
        public JsonResult MyMethod(MyObject data)
        {
            // your logic here
        }
    }