Reputation: 673
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();
Upvotes: 17
Views: 5442
Reputation: 1667
I tried this, but it still makes a new login when I swap slots. And user will potentially loose what they were doing.
I can see the blob data-protection-key is made fine in the storage account.
I'm using openidconnect and have a setup like this for authentication (just a PoC made from newest asp.net core web app template):
var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ') ?? builder.Configuration["MicrosoftGraph:Scopes"]?.Split(' ');
var aadSection = builder.Configuration.GetSection("AzureAd");
var connectionString = builder.Configuration["DataProtection:ConnectionString"];
var containerName = builder.Configuration["DataProtection:ContainerName"];
var applicationName = builder.Configuration["DataProtection:ApplicationName"];
var blobName = builder.Configuration["DataProtection:BlobName"];
builder.Services.AddDataProtection()
.SetApplicationName(applicationName) // This is optional. See below.
.PersistKeysToAzureBlobStorage(connectionString, containerName, blobName);
// Add services to the container.
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddMicrosoftIdentityWebApp(identityOptions =>
{
identityOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
identityOptions.Domain = aadSection.GetValue<string>("Domain");
identityOptions.TenantId = aadSection.GetValue<string>("TenantId");
identityOptions.ClientId = aadSection.GetValue<string>("ClientId");
identityOptions.CallbackPath = aadSection.GetValue<string>("CallbackPath");
identityOptions.Instance = aadSection.GetValue<string>("Instance");
identityOptions.ClientSecret = aadSection.GetValue<string>("ClientSecret");
},
options =>
{
options.Cookie.Name = ".AspNet.SharedCookie";
options.DataProtectionProvider = builder.Services.BuildServiceProvider().GetRequiredService<IDataProtectionProvider>(); ;
// Additional cookie options if needed
}
)
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
.AddInMemoryTokenCaches();
Am I missing something?
Upvotes: 0
Reputation: 125
Thanks Brian. I have this working now. I think it required both this and the machine key in the web configuration file. I say I think because after I swap slots and then click the browse button I still see my register and login buttons instead of the profile info but as soon as I navigate to another page I see the profile info. I appreciate the response.
Upvotes: 0
Reputation: 32009
I updated EricksonG's answer to include some new libraries. This solves the problem and it's quick to implement.
You will need these packages:
Microsoft.AspNetCore.DataProtection
Azure.Extensions.AspNetCore.DataProtection.Blobs
Azure.Extensions.AspNetCore.DataProtection.Keys
Here's my version of the code.
var connectionString = configuration["DataProtection:ConnectionString"];
var containerName = configuration["DataProtection:ContainerName"];
var applicationName = configuration["DataProtection:ApplicationName"];
var blobName = configuration["DataProtection:BlobName"];
services.AddDataProtection()
.SetApplicationName(applicationName) // This is optional. See below.
.PersistKeysToAzureBlobStorage(connectionString, containerName, blobName);
You might need to make sure the container exists, however this will definitely handle creating blob and the keys automatically. You never even have to see them.
Also: docs suggests this should go in your config before anything else that might be doing auth, because those tools sometimes have their own take on Data Protection.
AppSettings:
{
"DataProtection": {
"ApplicationName": "AppName", // Can technically be anything. See below for details on this.
"ConnectionString": "<Your key from azure>",
"ContainerName": "data-protection", // All lower case with dashes and numbers. There's no need to change this, but you can.
"BlobName": "data-protection-keys" // Same.
}
}
Protection: As-is, this is not all that secure. Get it working, and then if you need more security, take this further by doing Key Vault protection via .ProtectKeysWithAzureKeyVault(), or one of the other extension methods. It would also be a good idea to use KeyVault for the connection string, of course.
Application Name: The .SetApplicationName() line is optional; it helps isolate or share data between apps. From the docs: by default, the Data Protection system isolates apps from one another based on their content root paths, even if they're sharing the same physical key repository. This prevents the apps from understanding each other's protected payloads.
Upvotes: 4
Reputation: 472
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"
}
}
Upvotes: 10
Reputation: 673
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:
And OMG finally we have it working. Enjoy the 500% decrease in lost password customer service requests ;)
Upvotes: 23