Search code examples
c#amazon-web-servicesauthenticationasp.net-core-2.1

Cookie authentication issue ASP.NET CORE 2.1. AWS load balancing


UPDATE: It looks like following code is failing:

services.AddDataProtection()
            .SetApplicationName(appname)
            .PersistKeysToRedis(redis, "DataProtectionKeys")
            .ProtectKeysWithCertificate(LoadCert(Configuration));

It's unable to read certificate from the pfx file.

UPDATE2: Oh my! Certificate file has been excluded by .gitignore!:)) Live&Learn. At least we lived, right!?;))


Initial question: I have ASP.NET Core 2.1 app deployed behind AWS load balancer in Docker container. When I'm trying to log in into app from the login page. I am getting InvalidOperationException with this justification:

No authenticationScheme was specified, and there was no DefaultChallengeScheme found.

But when I hit same URL again, it actually moving to the proper page and works for a while, then again throws same exception with HTTP status 500 and after 2nd attempt to open same page it succeeds. Interestingly enough Chrome is not as robust as IE: if IE unable to recover after exception Chrome always return 404 on subsequent submission of the page, which produced aforementioned exception.

So I would appreciate, if somebody would be able to provide me with ideas how to remedy the situation Obviously issue is related to the authentication, but I could not figure out exactly what should be done.

Here is relevant exert from ConfigureServices() in Startup.cs:

        string appname = "MyApp";
        var redis = ConnectionMultiplexer.Connect(Configuration.GetConnectionString("RedisConnection"));
        services.AddDataProtection()
            .SetApplicationName(appname)
            .PersistKeysToRedis(redis, "DataProtectionKeys")
            .ProtectKeysWithCertificate(LoadCert(Configuration));

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        services.AddAuthentication( CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
                    options =>
                    { 
                        options.LoginPath = new PathString("/Area/Ctrl/Login");
                        options.LogoutPath = new PathString("/Area/Ctrl/Logout");
                        options.Cookie.IsEssential = true;
                    });

        services.AddDistributedRedisCache(o =>
        {
            o.Configuration = Configuration.GetConnectionString("RedisConnection");
        });
        services.AddSession(options =>
        {
            options.Cookie.Name = appname;
            options.IdleTimeout = TimeSpan.FromSeconds(600);
        });

Here is relevant code from Configure() in Startup.cs:

        app.UseForwardedHeaders();
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseAuthentication();
        app.UseSession();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
             name: "areas",
             template: "{area:exists}/{controller=Ctrl}/{action=Login}/{id?}"
           );
        }); 

Here is how I am setting principal in controller, which handles login:

            ClaimsIdentity identity = new ClaimsIdentity(GetUserRoleClaims(dbUserData), CookieAuthenticationDefaults.AuthenticationScheme);
            ClaimsPrincipal principal = new ClaimsPrincipal(identity);
            if (principal == null)
                throw new ApplicationException($"Could not create principal for {user?.UserName} user.");

            await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
            if (httpContext.User == null)
            {
                httpContext.User = principal;
            }

Solution

  • Ok, everything is working now.:) This is what made a difference:

    1. If app is under load balancing all instances has to share Data Protection encryption keys(e.g. use same key ring). Hence comes the Redis and a cert. Session should also be shared. Hence comes the Redis again.

    2. Certificate for ProtectKeysWithCertificate() call should load correctly. If it could not be loaded do not make that call at all, but that's would be really bad idea. Just figure out why it's not loading.

    3. To avoid InvalidOperationException being thrown in custom authentication HttpContext.User should be assigned manually inside Login action.

    One important thing about certificate: Data Protection module supports only certs with CAPI private keys. CNG ones are left behind.