13
votes

Whenever I updated my ASP.NET Core RC2 website running on as an Azure Web App, it logs out all users. It seems to be related to swapping a staging deployment slot to production (I use web deploy from VS to staging, and have it set to auto-swap to production). If I do a direct update of the production slot it's fine, but I don't want to do that. I am at a loss as to how to configure this, help would be appreciated!

Here is how I have it configured right now, my site only allows logging in directly (no facebook login etc.):

In ConfigureServices in Startup

// found some post that said this would help... doesn't seem to work...
services.AddDataProtection()
        .SetApplicationName("myweb");

services.AddIdentity<MyUser, MyRole>(options =>
{
    options.Cookies.ApplicationCookie.CookieDomain = settings.CookieDomain; // cookie domain lets us share cookies across subdomains
    options.Cookies.ApplicationCookie.LoginPath = new PathString("/account/login");
    options.Cookies.ApplicationCookie.ReturnUrlParameter = "ret";
    options.Cookies.ApplicationCookie.CookieSecure = CookieSecureOption.Never; // TODO: revisit site-wide https

    // allow login cookies to last for 30 days from last use
    options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(60);
    options.Cookies.ApplicationCookie.SlidingExpiration = true;

    // I think this needs to at least be longer than cookie expiration to prevent security stamp from becoming invalid before the cookie?
    options.SecurityStampValidationInterval = TimeSpan.FromDays(90);
})
.AddUserStore<MyUserStore>() // custom stores to hook up our old databases to new identity system
.AddRoleStore<MyRoleStore>()
.AddDefaultTokenProviders();

And in Configure in Startup

app.UseIdentity();
2
Are your staging and production slots using the same database? The login sessions are stored in the database, so if you are using 2 databases for 2 slots and do not keep those database settings while swapping, those login sessions are swapped too.Jack Zeng
Both slots are using the same database, but I don't think that this is relevant in this case... our site doesn't use any session information or persist any session information to the database. What is happening probably is that the login cookie is becoming invalid on swap... my guess is that something is causing the cookies to be encrypted differently on the different slots...Yellowfive
A little more detail: when I swap the deployment slots, the login cookies are still there in the browser... the website just doesn't think they are valid and ignores them.Yellowfive

2 Answers

20
votes

After much research... I think that I have this working.

So for anyone who wants an ASP.NET Core RC2 website that uses the Identity stuff for login, and wants to host it on an Azure Web App, and wants to use the Deployment Slots to do updates via swapping, and doesn't want every user to get logged out every time the website is updated... read on!

** Usually, Azure gives you some magical default configuration that makes all of the instances in a single Web App work together. The issue with deployment slots is that it essentially acts like two completely separate Web Apps, so all the magic is gone.

You need to configure Data Protection correctly to make this work. It is a bit confusing because the documentation for .NET Core Identity makes no explicit mention of depending on or requiring that you configure Data Protection correctly, but it does. Data Protection is what it uses under the hood to encrypt the application login cookie.

The following code is needed in ConfigureServices:

services.AddDataProtection()
    .SetApplicationName("myweb")
    .ProtectKeysWithCertificate("thumbprint");

services.AddSingleton<IXmlRepository, CustomDataProtectionRepository>();

Explanation of each piece:

  1. Setting the application name lets you share the protected data across multiple applications that use this same application name. May not be required for all scenarios, but doesn't hurt for ours.
  2. You need to use a custom key encryption method that is consistent across both deployment slots. The default is specific to each deployment slot and can only be used within that slot. If you look at key encryption at rest, Azure uses Windows DPAPI by default. Not gonna work for our purposes. So I chose to use a certificate, just enter the thumbprint as seen in the Azure portal. NOTE: for those who hate certificates and all the jargon around it, the .NET Core documentation says you need a X.509 certificate that supports CAPI private keys or it won't work. blah blah blah blah use the SSL certificate you got for your website, it should work just fine.
  3. An aside: you have to do some extra googling to actually make using the certificate work. The Data Protection documentation kind of leaves you hanging in the case of Azure... just using the code above, you will likely get a 500 error when you deploy to Azure. Firstly, make sure you have uploaded your certificate in the "Custom domains and SSL" section of your Web App. Secondly, you need to add the WEBSITE_LOAD_CERTIFICATES Application Setting and add the thumbprint of your certificate to that in order to use it. See using certificates in azure websites.
  4. Once you set a certificate to encrypt the data... it blows away any default configuration about where to store the data -- Azure stores it in a shared folder that all of your instances can access by default (defaults described here data protection defaults). But different deployment slots are separate... so the built-in file system and registry options are no help. You have to write a custom implementation as described here: key storage providers. But oh wait... the section at the bottom on custom key repositories is a 1-liner with no link or explanation about how to hook it up... you really need to read here: key management, go to the IXmlRepository section. Unfortunately the IDataProtectionBuilder has handy extensions for everything except what you need to do here, thus the line where we register this custom IXmlRepository with the service provider. Despite the alarmingly generic name of that interface, it only impacts Data Protection and won't mess with your other stuff.
  5. Not shown is the implementation of CustomDataProtectionRepository. I used Azure blob storage. It is a pretty simple interface, make a comment if you need help with that though.

And OMG finally we have it working. Enjoy the 500% decrease in lost password customer service requests ;)

9
votes

I tried to piece together a number of articles include the one here into a complete solution. Here is what I came up with. Original blog post: http://intellitect.com/staying-logged-across-azure-app-service-swap/

// Add Data Protection so that cookies don't get invalidated when swapping slots.
string storageUrl = Configuration.GetValue<string>("DataProtection:StorageUrl");
string sasToken = Configuration.GetValue<string>("DataProtection:SasToken");
string containerName = Configuration.GetValue<string>("DataProtection:ContainerName");
string applicationName = Configuration.GetValue<string>("DataProtection:ApplicationName");
string blobName = Configuration.GetValue<string>("DataProtection:BlobName");

// If we have values for all these things set up the data protection store in Azure.
if (storageUrl != null && sasToken != null && containerName != null && applicationName != null && blobName != null)
{
    // Create the new Storage URI
    Uri storageUri = new Uri($"{storageUrl}{sasToken}");

    //Create the blob client object.
    CloudBlobClient blobClient = new CloudBlobClient(storageUri);

    //Get a reference to a container to use for the sample code, and create it if it does not exist.
    CloudBlobContainer container = blobClient.GetContainerReference(containerName);
    container.CreateIfNotExists();

    services.AddDataProtection()
        .SetApplicationName(applicationName)
        .PersistKeysToAzureBlobStorage(container, blobName);
}

Here is a sample appsettings.json if they are stored that way.

{
  "DataProtection": {
    "ApplicationName": "AppName",
    "StorageUrl": "https://BlobName.blob.core.windows.net",
    "SasToken": "?sv=YYYY-MM-DD&ss=x&srt=xxx&sp=xxxxxx&se=YYYY-MM-DDTHH:MM:SSZ&st=YYYY-MM-DDTHH:MM:SSZ&sip=a.b.c.d-w.x.y.z&spr=https&sig=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "ContainerName": "data-protection-container-name", // All lower case with dashes and numbers.
    "BlobName": "data-protection-blob-name"
  }
}