2

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.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
MatusMak
  • 560
  • 4
  • 20
  • First I'd like to know why you'd do it this way without relying on established TLS ? – Illuminati Oct 01 '16 at 23:15
  • This is going to be on top of TLS - another layer. – MatusMak Oct 01 '16 at 23:17
  • Sorry I can't see anything wrong with the code. But feels like you're using WebAPI for the wrong thing. Basically you are tying to re-invent what WCF does with certificate trust. – Illuminati Oct 01 '16 at 23:40
  • In our case, we are working with very sensitive information that requires additional encryption layer. TLS is enabled as well. Bank applications are using similar techniques. – MatusMak Oct 02 '16 at 08:37
  • @MatusMak, any feedback re answer provided? – Nkosi Oct 03 '16 at 21:40
  • @Nkosi sorry for not replying, I'll leave a comment to your answer tomorrow ;) – MatusMak Oct 04 '16 at 14:36
  • @MatusMak has this been resolved. – Nkosi Feb 07 '17 at 12:40
  • @Nkosi yes and no - I got no answer for my question, but I had resolved my issue with custom workaround that only suits my needs. – MatusMak Feb 07 '17 at 12:58
  • @MatusMak, understood. You should add a brief summary of your findings as an answer. It could help others in a similar situation. – Nkosi Feb 07 '17 at 13:01
  • @Nkosi is it worth posting even when it's not a pretty solution? – MatusMak Feb 07 '17 at 13:11
  • @MatusMak if it solved your problem than it is worth posting as it may solve it also for someone who may come along with a similar problem. – Nkosi Feb 07 '17 at 13:13

2 Answers2

5

I created some middle ware to test changing OWIN Request.Body in the OWIN pipeline

public class DecryptionMiddleWare : OwinMiddleware {
    private string expected;
    private string decryptedString;

    public DecryptionMiddleWare(OwinMiddleware next, string expected, string decryptedString)
        : base(next) {
        this.expected = expected;
        this.decryptedString = decryptedString;
    }

    public async override System.Threading.Tasks.Task Invoke(IOwinContext context) {
        await DecryptRequest(context);

        await Next.Invoke(context);
    }

    private async Task DecryptRequest(IOwinContext context) {
        var request = context.Request;
        var requestBody = new StreamReader(request.Body).ReadToEnd();
        Assert.AreEqual(expected, requestBody);
        //Fake decryption code
        if (expected == requestBody) {
            //replace request stream to downstream handlers
            var decryptedContent = new StringContent(decryptedString, Encoding.UTF8, "application/json");
            var requestStream = await decryptedContent.ReadAsStreamAsync();
            request.Body = requestStream;
        }
    }
}

public class AnotherCustomMiddleWare : OwinMiddleware {
    private string expected;
    private string responseContent;

    public AnotherCustomMiddleWare(OwinMiddleware next, string expected, string responseContent)
        : base(next) {
        this.expected = expected;
        this.responseContent = responseContent;
    }

    public async override System.Threading.Tasks.Task Invoke(IOwinContext context) {
        var request = context.Request;
        var requestBody = new StreamReader(request.Body).ReadToEnd();

        Assert.AreEqual(expected, requestBody);

        var owinResponse = context.Response;
        // hold on to original stream
        var owinResponseStream = owinResponse.Body;
        //buffer the response stream in order to intercept downstream writes
        var responseBuffer = new MemoryStream();
        owinResponse.Body = responseBuffer;

        await Next.Invoke(context);

        if (expected == requestBody) {
            owinResponse.ContentType = "text/plain";
            owinResponse.StatusCode = (int)HttpStatusCode.OK;
            owinResponse.ReasonPhrase = HttpStatusCode.OK.ToString();

            var customResponseBody = new StringContent(responseContent);
            var customResponseStream = await customResponseBody.ReadAsStreamAsync();
            await customResponseStream.CopyToAsync(owinResponseStream);

            owinResponse.ContentLength = customResponseStream.Length;
            owinResponse.Body = owinResponseStream;
        }

    }
}

And then created an in memory OWIN integration test to see how the data passes through the middle ware, testing that the correct data is being received.

[TestMethod]
public async Task Change_OWIN_Request_Body_Test() {
    var encryptedContent = "Hello World";
    var expectedResponse = "I am working";

    using (var server = TestServer.Create<Startup1>()) {

        var content = new StringContent(encryptedContent);
        var response = await server.HttpClient.PostAsync("/", content);
        var result = await response.Content.ReadAsStringAsync();

        Assert.AreEqual(expectedResponse, result);
    }
}

public class Startup1 {
    public void Configuration(IAppBuilder appBuilder) {
        var encryptedContent = "Hello World";
        var decryptedString = "Hello OWIN";
        var expectedResponse = "I am working";
        appBuilder.Use<DecryptionMiddleWare>(encryptedContent, decryptedString);
        appBuilder.Use<AnotherCustomMiddleWare>(decryptedString, expectedResponse);
    }
}

It passed the test which proved that the data can be passed through the OWIN pipeline.

Ok, so next I wanted to see if it would work with web api. so created a test api controller

public class TestController : ApiController {
    [HttpPost]
    public IHttpActionResult Post([FromBody]string input) {
        if (input == "Hello From OWIN")
            return Ok("I am working");

        return NotFound();
    }
}

And configured a new startup to use web api and custom dencryption middle ware.

public class Startup2 {
    public void Configuration(IAppBuilder appBuilder) {
        var encryptedContent = "Hello World";
        var decryptedString = "\"Hello From OWIN\"";
        appBuilder.Use<DecryptionMiddleWare>(encryptedContent, decryptedString);

        //Configure Web API middleware
        var config = new HttpConfiguration();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        appBuilder.UseWebApi(config);
    }
}

Here is the in memory integration test

[TestMethod]
public async Task Change_OWIN_Request_Body_To_WebApi_Test() {
    var encryptedContent = "Hello World";
    var expectedResponse = "\"I am working\"";

    using (var server = TestServer.Create<Startup2>()) {

        var content = new StringContent(encryptedContent, Encoding.UTF8, "application/json");
        var response = await server.HttpClient.PostAsync("api/Test", content);
        var result = await response.Content.ReadAsStringAsync();

        Assert.AreEqual(expectedResponse, result);
    }
}

Which also passed.

Take a look at the sample code above and see if it provides any insight to where you went wrong with the example in your question.

Also remember to make sure that you put you custom middleware early in the pipeline before the web api middleware.

Hope it helps

Nkosi
  • 235,767
  • 35
  • 427
  • 472
4

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
    }
}
MatusMak
  • 560
  • 4
  • 20