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