Skip to main content

How to Resolve ASP.NET Core Key Protection Ring Problem in AWS Lambda

Introduction

When it comes to server less web application design using asp.net core razor pages, we definitely need to consider a factor of data protection key management and its lifetime in asp.net core. I developed a site using AWS toolkit of ASP.NET Core Razor Pages. The main advantage of ASP.NET Core is cross-platform from where we can deploy our application in MAC, Linux or windows. I deployed my site initially in IIS Server from which I got the results as expected .but later period I decided to host my site in AWS Lambda in order to meet our client requirement. Strangely, I got unexpected behavior from my site. I just refer the cloud information Lambda Log to identify or pinpoint the case, I got the error Information like “Error Unprotecting the session cookie” from the log. In this article, I tried to explain the root cause of the problem and its solution to overcome such kind of issue.

Data Protection in ASP.NET Core

This is feature in ASP.NET Core which acts as replacement for Machine Key element in order to support modern web applications, this act as cryptographic API to developers. What makes the difference from old cryptographic mechanism is that we can embed our own cryptographic mechanisms. So that encryption and decryption of data will act according to the specified cryptographic mechanisms in our application else takes default on its own.
When an application runs, the data protection for that system will be set by default on the basis of operating system.
The web application has its own key ring which is highly secured and this will appropriate to the single machine. If our application is sp
read across the machine and definitely we will have different scene in order to store Key storage location. We have various methods to handle this situation   which are as follows
  • Ephemeral storage (In-Memory, short live)
  • SQL Server
  • Redis
  • Azure storage 
  • File System
  • Windows Registry(Only IIS  or windows deployments)
In above mentioned methods, Ephemeral storage is not suit for web farm scenarios. In that occasion, we can choose SQL Server, Redis, Azure storage or File System (Shared Network) to store the Key Storage. So that Load balancer will take no effect on Data Protection without problem.

The stunning feature of data protection mechanism is that the Key that is generated which is automatically handled by itself which has an lifetime of 90 days .once it’s expired it will automatically spin-off.

The application will chooses the default key from the key ring and new keys will be generated once the existing is expired. The default expiration date of key is 90 days or else we can configure it in our application like this in our startup class of ASP.NET core

        services.AddDataProtection()
       // use 14-day lifetime instead of 90-day lifetime
       .SetDefaultKeyLifetime(TimeSpan.FromDays(14));


Issue Scenario

ASP.NET Core Razor Pages is hosted in AWS Lambda. When navigate from one page to another page for the first time, we got 400 responses Error because the request is not accepted by anti-forgery token. Hence I tried it second time and it is working as expected. I was scratching my head for week to address this issue. I have tried various approaches to find a solution, which I have explained in detail

Approach 1 – Disable Anti-forgery Token

I know this is a bad idea, but I was clueless and so I did disabling anti forgery token and 400 bad request error disguised, then I didn’t get any values from tempdata . When I checked the cloud information log , then  I got some information from request log as below

[Warning] Microsoft.AspNetCore.DataProtection.Repositories.EphemeralXmlRepository: Using an in-memory repository. Keys will not be persisted to storage.
[Warning] Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager: Neither user profile nor HKLM registry available. Using an ephemeral key repository. Protected data will be unavailable when application exits.
[Information] Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager: Creating key {fbf9442c-a80b-43c0-b898-16f94d1d532b} with creation date 2019-07-30 12:23:07Z, activation date 2019-07-30 12:23:07Z, and expiration date 2019-10-28 12:23:07Z.
[Warning] Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager: No XML encryptor configured. Key {fbf9442c-a80b-43c0-b898-16f94d1d532b} may be persisted to storage in unencrypted form.
[Warning] Microsoft.AspNetCore.Session.SessionMiddleware: Error unprotecting the session cookie.


Approach 2 – Using Redis Cache

It is very important to note the data protection in lambda of razor pages. It is cryptographic mechanism in which all cookies related information and anti-forgery tokens will be encrypted by the system generated key in asp.net core and the same key will be used to decrypt the information. In ASP.NET Core there are various key storage locations as explained above. By default, keys will be stored in Windows Registry in IIS Server but in the Lambda, the scene is different .because there are no windows registry will be there and so we are getting such error like “Error in Unprotecting the session cookie”

In ASP.NET Core data protection terms, Protected means encryption and Unprotected means decryption. It is clearly describing that there is error in Unprotecting or decrypting the session cookie. In Lambda as there is no persistence storage instead it will be using ephemeral key repository which will be challenge to lambda in storing protection key which is inevitable.

Handling in-memory Session Data in Redis

In order to implement the redis cache session management, we need to install the extension of distributed redis cache using nugget manager as follows

Then we must update the services with distributed redis cache in startup.cs in asp.net core
Once you have configured the distributed redis cache then we must use Use session middleware, then session management of lambda will be stored in redis cache.

Handling Protection Key in IXmlRepository

Lot of Key storage location are available but we decided to store persistence key storage location as Mysql, certainly we can do this by entity framework core. Let’s see how we can implement this in action.
First, we have to design the POCO Class of DataProtection Key

    public class DataProtectionKey
    {
        [Key]
        public string FriendlyName { get; set; }
        public string XmlData { get; set; }
    }


Next, we need to implement the IXmlrepository which will be as follows

   public class DataProtectionKeyRepository : IXmlRepository
    {
        private readonly AppDbContext _db;

        public DataProtectionKeyRepository(AppDbContext db)
        {
            _db = db;
        }

        public IReadOnlyCollection GetAllElements()
        {
            return new ReadOnlyCollection(_db.DataProtectionKeys.Select(k => XElement.Parse(k.XmlData)).ToList());
        }

        public void StoreElement(XElement element, string friendlyName)
        {
            var entity = _db.DataProtectionKeys.SingleOrDefault(k => k.FriendlyName == friendlyName);
            if (null != entity)
            {
                entity.XmlData = element.ToString();
                _db.DataProtectionKeys.Update(entity);
            }
            else
            {
                _db.DataProtectionKeys.Add(new DataProtectionKey
                {
                    FriendlyName = friendlyName,
                    XmlData = element.ToString()
                });
            }

            _db.SaveChanges();
        }
    }


In order to initiate connection between portal and Mysql database we must implement AppDbContext

public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions options) : base(options) { }

        public DbSet DataProtectionKeys { get; set; }
    }


Finally, we must include AppDBContext and Custom Key XMLRepository in startup.cs as follows,
Startup.cs

namespace MyPortal.Web
{
    public class Startup
    {      

        public Startup(IConfiguration configuration, IHostingEnvironment env)
        {
            Configuration = configuration;         

        }

        public  IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            var connectionString = Configuration["mysqlconnection:connectionString"].ToString();
            services.AddDbContext(o => o.UseMySQL(connectionString));

            services.Configure(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });
            services.Configure(options =>
            {
                options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
            });

            services.AddSingleton();
            services.AddAntiforgery(o => o.HeaderName = "XSRF-TOKEN");

            services.AddSingleton();

            //services.AddMemoryCache();
            var built = services.BuildServiceProvider();
            services.AddDataProtection().AddKeyManagementOptions(options => options.XmlRepository = built.GetService());

            services.AddDistributedRedisCache(options =>
            {
                options.InstanceName = "redisinstancename";
                options.Configuration = "redishIp:portNo";
            });

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1).AddSessionStateTempDataProvider().AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());


            services.AddSession(opt =>
            {
                opt.Cookie.IsEssential = true;
                opt.IdleTimeout = TimeSpan.FromHours(1);
            });


        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {

            app.UseDeveloperExceptionPage();

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            // app.UseMvc();
            app.UseSession();
            app.UseCookiePolicy();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller}/{action}/{id?}",
                    defaults: new { controller = "Feedback", action = "Create" });
            });
           
           
        }
    }
}


Note, to use MySQL as your repository, we must include the Entity framework core package for MySQL

Conclusion

Once you have configured the Persistence storage for protection key as MySQL. All error on Unprotecting session cookie disguised and my application works fine without any issues. I think this article is helpful to you in resolving the persistence storage issue. If any feedback, please comment which will be helpful for me to shape the article in best way.

Comments

Post a Comment

Popular posts from this blog

How to resolve ASP.NET core web API 2 mins timeout issue

Introduction We are in the new world of microservices and cross-platform applications which will be supported for multiple platforms and multiple heterogeneous teams can work on the same application. I like ASP.NET Core by the way its groomed to support modern architecture and adhere to the software principles. I am a big fan of dot net and now I become the craziest fan after seeing the sophisticated facility by dot net core to support infrastructure level where we can easily perform vertical and horizontal scaling. It very important design aspect is to keep things simple and short and by the way, RESTFul applications are build and it is a powerful mantra for REST-based application and frameworks. Some times we need to overrule some principles and order to handle some situations. I would like to share my situation of handling HTTP long polling to resolve the ASP.Net core 2 mins issue. What is HTTP Long polling? In the RESTFul term, when a client asks for a query from the serv

Which linq method performs better: Where(expression).FirstorDefault() vs .FirstOrDefault(expression)

 Introduction When it comes to LINQ, we always have multiple options to execute the query for the same scenario. Choosing correct one is always challenging aspect and debatable one. In one of our previous articles   Any Vs Count  , we have done performance testing about best LINQ methods over .NET types. In this article, I would like to share about  Where(expression).FirstorDefault() vs .FirstOrDefault(expression) Approaches Performance testing for  Where(expression).FirstorDefault() vs .FirstOrDefault(expression) is very interesting IEnumerable<T> or ICollcetion<T>  .FirstOrDefault(expression) is better than  Where(expression).FirstorDefault() Public API To check the performance, I need some amount of data which should already available. So I decided to choose this  public api . Thanks to publicapis Public API Models Entry class using System ; using System.Collections.Generic ; using System.Text ;   namespace AnyVsCount { public class Entry { pub