Search code examples
c#asp.net-mvcazureazure-blob-storageazure-keyvault

ASP.NET Cookie Keys Persisted to Azure Blob Storage Not Working Across Multiple Applications


I was recently tasked with changing our auth application (.NET 4.7.2) that serves as the identity provider for four other applications (3 are 4.7.2, one is .NET 7) from storing our keys on a file share to storing them in Azure. In this case, storing the keys in a blob and protecting the keys using key vault. Previously, we stored our keys on a file share, as this is a web farm.

Below is our original code, which was almost identical across all five applications (the .NET 7 app is slightly different):

var dataProtector = DataProtectionProvider.Create(
new DirectoryInfo(ConfigurationManager.AppSettings["SharedKeyDirectory"]))
.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
                            "Cookies", "v2");

var ticketDataFormat = new AspNetTicketDataFormat(
               new DataProtectorShim(dataProtector));

app.UseCookieAuthentication(new CookieAuthenticationOptions {
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    CookieDomain = ConfigurationManager.AppSettings["CookieDomain"],
    CookieName = ConfigurationManager.AppSettings["CookieName"],
    TicketDataFormat = ticketDataFormat,
    CookieManager = new ChunkingCookieManager(),
    LoginPath = new PathString("/../Auth/Account/Logon"),
    Provider = new CookieAuthenticationProvider {
        OnApplyRedirect = ctx =>
        {
            if (bool.Parse(ConfigurationManager.AppSettings["ApplyHttpsRedirect"] ?? "false")) {
                ctx.RedirectUri = ctx.RedirectUri.Replace("http:", "https:");
            }
            ctx.Response.Redirect(ctx.RedirectUri);
        }
    }
});

The code above was persisting unencrypted keys to a file share. All of our 4.7.2 applications had this exact block of code defined so that they would all use the same cookies.

I've since changed this code to the following:

var ticketShim = new DataProtectorShim(
            DataProtection.CreateProvider((d) => {
                d.PersistKeysToAzureBlobStorage(ConfigurationManager.AppSettings["DataProtectionBlobUri"],
                ConfigurationManager.AppSettings["DataProtectionBlobContainer"], ConfigurationManager.AppSettings["DataProtectionBlobName"])
                .ProtectKeysWithAzureKeyVault(new Uri(ConfigurationManager.AppSettings["DataProtectionKeyUri"]),
                new ClientSecretCredential(ConfigurationManager.AppSettings["AzureTenantId"],
                    ConfigurationManager.AppSettings["AzureClientId"],
                    ConfigurationManager.AppSettings["AzureClientSecret"]));
            })
                .CreateProtector(
                    "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
                    "Cookies",
                    "v2"));

var ticketDataFormat = new AspNetTicketDataFormat(ticketShim);

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    CookieDomain = ConfigurationManager.AppSettings["CookieDomain"],
    CookieName = ConfigurationManager.AppSettings["CookieName"],
    TicketDataFormat = ticketDataFormat,
                
    CookieManager = new ChunkingCookieManager(),
    LoginPath = new PathString("/../Auth/Account/Logon"),
    Provider = new CookieAuthenticationProvider
    {
        OnApplyRedirect = ctx =>
        {
            if (bool.Parse(ConfigurationManager.AppSettings["ApplyHttpsRedirect"] ?? "false"))
            {
                ctx.RedirectUri = ctx.RedirectUri.Replace("http:", "https:");
            }
            ctx.Response.Redirect(ctx.RedirectUri);
        }
    }
});

All of the applications have the same block of code copy-pasted into their Startup classes, and all 4.7.2 application have the same values copy-pasted into their web.config files.

The only differences here is the auth application includes a few more features, such as the ApplicationUserManager class, which is used to regenerate user identities when they're re-validated. That code is just:

var userIdentity = await CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);

var cookieAuthOptions = new CookieAuthenticationOptions {
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    CookieDomain = ConfigurationManager.AppSettings["CookieDomain"],
    CookieName = ConfigurationManager.AppSettings["CookieName"],
    CookieHttpOnly = true,
    TicketDataFormat = ticketDataFormat,
    CookieManager = new ChunkingCookieManager(),
    LoginPath = new PathString("/Account/Logon"),
    Provider = new CookieAuthenticationProvider {
        // Enables the application to validate the security stamp when the user logs in.
        // This is a security feature which is used when you change a password or add an external login to your account.  
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, User>(
            validateInterval: TimeSpan.FromMinutes(30),
            regenerateIdentity: (manager, user) => manager.GenerateUserIdentityAsync(user)),
        OnApplyRedirect = ctx => {
            if (!(IsAjaxRequest(ctx.Request) || IsApiRequest(ctx.Request))) {
                if (bool.Parse(ConfigurationManager.AppSettings["ApplyHttpsRedirect"] ?? "false")) {
                    ctx.RedirectUri = ctx.RedirectUri.Replace("http:", "https:");
                }
                ctx.Response.Redirect(ctx.RedirectUri);
            }
        }
    },
    SlidingExpiration = true,
    ExpireTimeSpan = TimeSpan.FromMinutes(90)
};

I can open the auth application and log in, and I can see the encoded cookies are available. All of them are set for the domain "localhost", as expected. When I click on the link to one of the applications, though, which in this case would be localhost/IAM or localhost/PAM, I am automatically kicked back to the login page for the auth application, indicating that the cookie couldn't be seen/read/decoded by the "child" applications. There is nothing special in the IIS log - just a standard HTTP 401 error code.

I can see the xml file containing the keys here in blob storage: Blob storage of keys for auth, so I know they're being created and are accessible. I was able to download & view them, and they're not malformed in any way.

I am stumped. The way I see it, all we changed was that we store the keys in blob storage instead of a file share on an on-prem server. Even if I delete the blob and take out the Azure Key Vault protection, I get the same issue - logging in is fine, cookies are created and encoded, but the child apps return 401's and redirect automatically to auth to log back in.

Help :(

I've tried with and without key protection in key vault. I've deleted the blob more than once to verify it's not malformed.


Solution

  • I figured it out shortly after posting the question. In Auth, there is a line that I had to add to ensure TLS 1.2 for the connection to Azure, but I forgot to put that same line of code in all of the child apps:

    System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;