Information security in ASP.Net Core WebAPI

Introduction

Communicating between the different software and applications always was my favorite topic. In the past two decades, some technics and technologies have been raised; Technologies like RPC, WebService, SOAP, and REST. Each of these technologies has its own pros and cons. When transporting the information over the network and between the different technologies, some communication protocols should be get considered, like Handshaking, maximum allowed delay, transported information format, and Security. Each of these issues has its own problems and solutions. In this post, I will focus on the security issue in REST API technology in ASP.Net Core.

Problem

Transporting the information between the applications over the network can be dangerous, especially when they come with sensitive data like User name, Password, or other personal and critical information. To fix this issue, there are a bunch of solutions and tools, and the most common one is known as SSL/TLS. To protect the transported information against vulnerabilities (e.g. sniffing), the SSL/TLS methods encrypt them into the cipher format using the encryption algorithms like RSA and AES. These methods are largely reassuring, but when information security becomes the most important factor in the project, can we completely trust these methods? I think, NO! It may be a little pessimist, but there is always a backdoor or misused issue for security methods in the IT world, And this leads us to fasten our seatbelts tighter!

Suggested Solution

In this topic, we are going to work on a way to increase the security of the transmitted information over the REST API technology in an ASP.Net Core Web API application. Adding more security levels in an application makes feeling good about the sensitive data, but doing this in an inappropriate way can put the application in another problem and produce some pitfalls like additional lateness, especially when the requests grow in a short period of time. Another issue that can be produced in this case is increasing the complexity of the project. To handle most of these problems, a solution is creating an optional and customizable method to filter and manage the encrypted information. The idea behind this method tries to working on encrypting and decrypting the transmitted information in application level/layer. It helps us to encrypt/decrypt the entire or part of the received information from the client and deliver them in a plain text, JSON, or XML formats. To implement this method in an ASP.Net Core application, we have two options:

  • Receiving the encrypted data as a single input parameter of the action, and decrypting them in the action body
  • Creating an action filter to decrypt the information prior to the action call, and passing detailed and type-safe information as an input parameter into the action

I think the first option is straight forward. We should declare an object/string input parameter in our action declaration to receive an unknown data, then decrypt that as a ciphertext, and try to parse (deserialize) that into the required model in the action body. If the process failed in each step, we can simply return a Bad request (400) as the action result. Otherwise, we can continue using transmitted and deserialized information. This option can make the decryption and deserialization process painful because we have to do the same job in each action that needs to receive encrypted information. The second problem is about the action parameters. As I discussed above, to use this option, we should use an unknown data type instead of using a type-safe model, and it makes the unit test writing so hard. The next problem arises when we want to deliver documentation of our API to the clients in tools like swagger. There we should make another documentation to guide the users about the required data model structure.

Before discussing the second option, let’s take a look at the request flow in ASP.Net Core applications. As the Microsoft community members discussed in this post, there are five types of filters in the ASP.Net Core action invocation pipeline: Authorization filter, Resource filter, Action filter, Exception filter, and finally, Result filter. Figure 1 is from the Microsoft community and demonstrates a brief structure about the filters interact with each other.

Figure 1: Filter types interact in the filter pipeline

When an ASP.Net Core application receives a new request, the first job is checking the client credentials using the Authorization filters. If the request passes all of the credentials, then Resource filters begin their duties. The request body is pure when it enters into the Resource filter, so we can change it or do some other activities on it before binding that into the target action. This is the best place to decrypt and translate the received cipher content into the target action. After the Resource filters’ job done, the request gets delivered into the Action filters. These filters receive a well-formed data model and work on them to filter and check the model validations. We won’t go further with other filters in this article, because our idea should be done and implemented before them. Now let’s talk about the second option. As we saw before, the Action filters need a valid data format or well-formed data to bind them into the action parameters, but the encrypted data from the client doesn’t have one. To make the data readable in Action filters, we should do the steps in option one in Resource filters. To make it clearer, let’s have a look at its workflow:

  1. Creating a custom Resource Filter to catch the received data before the Action filters
  2. Try to decrypt the Request content
    1. Decryption succeed: Go to the next action
    2. Decryption failed: Stop the process and return the Bad request(400) as the result
  3. Try to deserialize the decrypted information into the type-safe object
    1. Deserialization succeed: Go to the next action
    2. Deserialization failed: Stop the process and return the Bad request(400) as the result
  4. Replace the deserialized type-safe object with the Request content
  5. Push forward the request to the next Resource and Action filters

The second option has some benefits against the first one. By creating a generic filter, we can use it anywhere we want, without needing to customize it every time, and also, we can decrease the bug probability by centralizing the functionality in a place.

Let’s start coding πŸ’“.

The solution in the action

To develop and test the code in a semi-real world, I will create a new ASP.Net Core Web Application following an API controller and some actions. The controller’s name is SampleServiceController, and it has an action with the name SampleAction. This action will be a POST method to accept the requests with the content. The following code block shows our API controller’s body:

using Microsoft.AspNetCore.Mvc;

namespace RESTEncryption.AspNetCoreWebApp.Controllers.Api
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class SampleServiceController : Controller
    {
        [HttpPost]
        public IActionResult SampleAction()
        {
            return Ok("The action invoked successfully.");
        }
    }
}

By running the application in Visual Studio, it will start on the localhost and a random port. There are a dozen ways to check and call the REST methods, but here, we use the Postman. After calling the newly created action using Postman, we should get a 200 status code and a success message as the action output.

Figure 2: API methods invocation result

To pass the data into SampleAction, the SampleModel class has been created that should contain all of our required properties. Some of its properties are confidential and others not. You can see its body in the following code block:

public class SampleModel
{
    public int Int32Property { get; set; }

    public string ConfidentialData { get; set; }

    public string NonConfidentialData { get; set; }
}

Then we will add an input parameter to our action in the API controller with a FromBody attribute to bind that to the request body. This time, when calling the method, I will set a JSON formatted text into the request body. The request body should be as follows:

{
	"Int32Property": 10,
	"ConfidentialData": "A secure text or value",
	"NonConfidentialData": "A non-secure text or value"
}

When receiving the request in the action, the input parameter should be bonded to an instance of the SampleModel class and all of the input values should be accessible in a type-safe structured format.

Figure 3: Input parameter in action call

Now, let’s check it with an encrypted body. To accomplish this, I will use the AES algorithm and encrypt the JSON body into the ciphertext. This time, the Content-Type parameter in the request header should change to text/plain from the application/json. When I send the request to the server, I get a nice 415 Unsupported media type error. This error arises because ASP.Net Core generally uses JSON deserializer to transform and deserialize the request body into the declared type. But there are some solutions to dictate it the other data types and structures as Rick Strahl has written in this post. But here, we don’t want to use the Raw data, and we have to transform the encrypted body into the valid JSON data then pass it to the next filters. As we talked before, to catch the incoming requests and access their body before binding them into the action requested data types, we should declare a Resource Filter and assign it to our action as a TypeFilterAttribute. To create a new ResourceFilter, I add a new class with the name RequestBodyDecryptionFilter that is inherited from the IResourceFilter and is accepts a generic type to use that in the deserialization task. This class would be inserted into a folder with the name Infrastructure. The following code snippet shows the raw class structure.

public class RequestBodyDecryptionFilter : IResourceFilter
{
    public void OnResourceExecuted(ResourceExecutedContext context)
    {
        // We don't need this method here
    }

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        // The business will be added here
    }
}

To get familiar with the inherited methods, you can use this and this links, but we will only use the OnResourceExecuting method to capture the request and manipulate its content. This method gets called before the OnResourceExecuted method and we want to make sure that valid data will be passed into this method and the others. To use this method, we should add a TypeFilterAttribute above our action and pass the RequestBodyDecryptionFilter type into it as the parameter.

[HttpPost]
[TypeFilter(typeof(RequestBodyDecryptionFilter))]
public IActionResult SampleAction([FromBody] SampleModel data)
{
      return Ok("The action invoked successfully.");
}

First thing first, to check the method behavior, I will call the action with valid JSON data. When the OnResourceExecuting method gets called, we can have access to the request body using its ResourceExecutingContext parameter and should read it as a stream. The following figure shows the request body:

Figure 4: Request body in ResourceFilter

As Figure 4 demonstrates, we can access the request body easily here. Now let’s send the request again, but this time as an encrypted text.

Figure 5: Encrypted request body in ResourceFilter

As you see, we received a ciphertext from the request body, and now we should decrypt it using the algorithm and key that we used to encrypt the content. To do so, I’ve added a helper class and inject it into the RequestBodyDecryptionFilter. This class has two public and two private methods to encrypt and decrypt the strings using the key that gets passed into the constructor. I pass the key into the EncryptionDecryptionHelper class in the Startup class when initializing and configuring it to be used in DI using the EncryptionDecryptionExtension class.

public class EncryptionDecryptionHelper
{

    private string _key;

    public EncryptionDecryptionHelper(string key)
    {
        _key = key;
    }

    public string EncryptString(string text)
    {
        try
        {
            // The ecryption process goes here
        }
        catch (Exception ex)
        {
            throw new Exception("An error has been occurred when encrypting the value.", ex);
        }
    }

    public string DecryptString(string cipherText)
    {
        try
        {
            // The decryption process goes here
        }
        catch (Exception ex)
        {
            throw new Exception("An error has been occurred when decrypting the value.", ex);
        }
    }
}

The EncryptionDecryptionConfig is a simple class that only has one property with the name Key and gets used to pass the key into the EncryptionDecryptionHelper.

public class EncryptionDecryptionConfig
{
    public string Key { get; set; }
}
public static class EncryptionDecryptionExtension
{
    public static IServiceCollection AddEncryptionDecryptionHelper(this IServiceCollection services, EncryptionDecryptionConfig config)
    {
        services.AddTransient(f =>
        {
            return new EncryptionDecryptionHelper(config.Key);
        });
        return services;
    }
}

Now we can add the EncryptionDecryptionHelper class into the DI service using the AddEncryptionDecryptionHelper extension method in the Startup class like this:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    { 
        services.AddControllersWithViews();
        var key = "some key to be used in encryption and decryption";
        services.AddEncryptionDecryptionHelper(new EncryptionDecryptionConfig { Key = key });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        ...
    }
}

Finally, we can get an instance of the EcryptionDecryptionHelper class in the RequestBodyDecryptionFilter and use it to decrypt the request body:

public class RequestBodyDecryptionFilter : IResourceFilter
{
    private EncryptionDecryptionHelper _encryptionDecryptionHelper;
    public RequestBodyDecryptionFilter(EncryptionDecryptionHelper encryptionDecryptionHelper)
    {
        _encryptionDecryptionHelper = encryptionDecryptionHelper;
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
        // We don't need this method here
    }

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        try
        {
            var request = context.HttpContext.Request;
            if (request.Body.CanSeek)
                request.Body.Position = 0;
            var reader = new StreamReader(request.Body);
            var encryptedContent = reader.ReadToEndAsync().Result;
            var decryptedContent = _encryptionDecryptionHelper.DecryptString(encryptedContent);
        }
        catch
        {
            context.Result = new BadRequestResult();
        }
    }
}

If you test the code, you can see the encryption works and you will get the plain text data. You should consider that the stream access, decryption, and parsing processes may produce some exceptions and errors. We can handle them separately by their type, but here, we simply use a global Exception type to handle all of them, then set a BadRequestResult(400) as the result.

The last thing we should do is converting back the plain text into the Byte array and set it back to the request body to be accessible via our action.

public void OnResourceExecuting(ResourceExecutingContext context)
{
    try
    {
        var request = context.HttpContext.Request;
        if (request.Body.CanSeek)
            request.Body.Position = 0;
        using StreamReader reader = new StreamReader(request.Body);
        var encryptedContent = reader.ReadToEndAsync().Result;
        var decryptedContent = _encryptionDecryptionHelper.DecryptString(encryptedContent);
        var newBody = Encoding.ASCII.GetBytes(decryptedContent);
        request.Body = new MemoryStream(newBody);

        if (request.Headers.ContainsKey("Content-Type"))
              request.Headers["Content-Type"] = new Microsoft.Extensions.Primitives.StringValues("application/json");
        else
              request.Headers.Add("Content-Type", new Microsoft.Extensions.Primitives.StringValues("application/json"));
    }
    catch
    {
        context.Result = new BadRequestResult();
    }
}

A small thing to notice is the content type that we use in our communication. To send the data in cipher formatted, we should use the text/plain in our request header with the key Content-Type (or skip it to be considered as the plain text), but the Asp.Net Core system uses the JSON content type by default to parse that into the required data type. To do that, we should change the Content-Type parameter in the request header into the application/json.

After performing these changes in the request, you can get a type-safe value in your input parameter in action.

Summary

The process of this article works on the fully encrypted content and tries to decrypt that into a valid format known by the ASP.Net. To customize this process, you can encrypt the request partially. What I mean is encrypting only the required fields and properties, and keeping other fields as simple data. To decrypt this type of content, you need to declare the filter class as a generic type to accept the required data type, then deserialize the content into it and replace the encrypted properties with the decrypted value. To find the confidential properties, you can create a custom attribute and assign that to the target property. This method is really customizable and can have different behavior in different scenarios or projects.

You can find a complete source code here.

2 thoughts on “Information security in ASP.Net Core WebAPI

  1. “Now let’s send the request again, but this time as an encrypted text.”

    Is there an example of how the request body is encrypted on the browser side?

    Like

    1. It depends on the caller, If you call the APIs using AJAX, or client side libraries, then you should encrypt the information or request body in the client side, then you will be able to track the encrypted/plane text information in the browser, otherwise, if you use another API/Application/Service to call them then you won’t

      Like

Leave a comment

Design a site like this with WordPress.com
Get started