Data Protection keys collection is intermittently modified causing 401 errors - authentication

I have a two sites that share an authentication cookie:
Main site that generates auth cookie
Chat site that hosts a SignalR Hub.
The Main site's cookie is sent through and used to authenticate SignalR negotiation requests.
I recently deployed some large scale changes moving from ASP.NET Framework to .NET5. Part of this change meant using the new Data Protection keys so the auth ticket on both apps could be decrypted (previously, the ASP.NET Framework version just needed the machine keys on both apps to match, which could be set in web.config).
Here's what the Data Protection code looks like, used in both the Main and Chat sites:
public void ConfigureServices(IServiceCollection services)
{
services.AddCustomDataProtection(_configuration);
...
}
public static IServiceCollection AddCustomDataProtection(this IServiceCollection services, IConfiguration configuration)
{
var siteSettings = configuration
.GetSection(SiteSettings.SectionName)
.Get<SiteSettings>();
var dataProtection = services.AddDataProtection()
.SetApplicationName(siteSettings.SiteName);
var dataProtectionSettings = configuration
.GetSection(DataProtectionSettings.SectionName)
.Get<DataProtectionSettings>();
if (!string.IsNullOrWhiteSpace(dataProtectionSettings.KeyVaultIdentifier))
{
dataProtection
.PersistKeysToAzureBlobStorage(
configuration.GetConnectionString("StorageConnectionString"),
dataProtectionSettings.KeyStoreContainerName,
dataProtectionSettings.KeyStoreBlobName)
.ProtectKeysWithAzureKeyVault(
new Uri(dataProtectionSettings.KeyVaultIdentifier),
new DefaultAzureCredential());
}
return services;
}
Since this was deployed, we've had some strange issues whereby the Data Protection keys blob stored in Azure Storage is modified throughout the day. It doesn't look like there's any rhyme or reason behind when this happens.
From my understanding, the Data Protection keys are by default refresh every 90 days or so, however this is happening 1-2 times a day. The code in the apps never interact with the Key Vault keys nor the Data Protection key blob directly (only via the code posted below) and I have added logs that show the correct settings are being passed through to the Data Protection extension methods.
On the other hand, I have been having intermittent issues where when the Main site sends an negotiate request to the Chat site, it receives a 401 error, indicating that the the cookie/auth ticket was invalid or otherwise couldn't be decrypted. I can reproduce this when the key is modified by restarting the Main site, logging out and then logging in again. I think these two problems are related.
My stab-in-the-dark explanation of this behaviour would be that perhaps the Main site is correctly picking up a change in the Data Protection keys, however the Chat site does not. The Chat site cannot then decrypt an auth ticket that was encrypted using a key it doesn't have access to. Restarting the Chat site fixes the issue, presumably because the Data Protection keys are reloaded at startup.
So I have two main concerns:
why is the Data Protection key blob being modified?
why is the Chat site not picking up the new keys if indeed new keys are being added to the collection of Data Protection keys?

The issue ended up being that both the Main and Chat sites stored the data protection keys in the same blob in Azure Storage. This caused the strange behaviour I was witnessing with the blob being modified, and in turn, the application errors I was experiencing.
I figured storing the keys in the same location would be fine due to my reading of the documentation at the time relating to the SetApplicationName() method causing the keys to be isolated from one another. Unfortunately that isolation did not extend to the actual physical data protection keys rather, just the payload stored inside of them. The documentation surrounding app isolation has since been updated and is now far more detailed.

Related

Random ASP.NET Core Identity Logouts in production

I have an ASP.NET Core MVC web application that uses Identity to handle user account Authentication and Authorization. When running the app on my local IIS express everything was working properly. After deploying the app to a shared web server I started to notice that the logged-in user accounts would get logged out at seemingly random intervals. Through experimentation, I was able to determine that the log-outs were occurring whether the account was active or idle. They were occuring at no recuring time interval and completely unrelated to any expiry time that I set on my cookies. The logouts were occuring on every view in the web app so I couldn't pin the issue to any particular controller. Also I use the same Database for the published and the local testing version of the app and therefore the same user accounts. I anyone has an idea where to start looking for a solution it would be greatly appreciated.
I posted this question because there is a great answer that 90% solves the issue Here however of the multiple forums that I have been scouring over the last few days there are none with an accepted answer. I am posting this answer to address this. The underlying cause of the issue is that IIS application pool is being reset or recyling and on a shared host with multiple applications using it this can be happening fairly frequently. As is suggested in the above link Data Protection has to be used to persist the keys if IIS application pool recycles. Here is the code offered in the original answer.
services.AddDataProtection()
.PersistKeysToFileSystem(new System.IO.DirectoryInfo("SOME WHERE IN STORAGE"))
//.ProtectKeysWithCertificate(new X509Certificate2());
.SetDefaultKeyLifetime(TimeSpan.FromDays(90));
This code is to be added in ConfigureServices in Startup.cs
As my application is being hosted on a shared server using .PersistKeysToFileSystem was not an option so instead I persisted the keys using DbContext like this:
services.AddDataProtection().PersistKeysToDbContext<MyKeysContext>()
.SetDefaultKeyLifetime(TimeSpan.FromDays(90));
Based on This article here I build MyKeysContext as follows.
// Add a DbContext to store your Database Keys
services.AddDbContext<MyKeysContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("MyKeysConnection")));
In ConfigureServices in Startup.cs and then created a class called MyKeysContext as follows:
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using WebApp1.Data;
namespace WebApp1
{
public class MyKeysContext : DbContext, IDataProtectionKeyContext
{
// A recommended constructor overload when using EF Core
// with dependency injection.
public MyKeysContext(DbContextOptions<MyKeysContext> options)
: base(options) { }
// This maps to the table that stores keys.
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
}
}
I created the database on my Host this will probably be different so I have omitted this step. then I applied the migrations to the database like this.
Add-Migration AddDataProtectionKeys -Context MyKeysContext
Update-Database -Context MyKeysContext

Azure SQL authenticate as user via API with MS Identity Platform

I have an ASP.NET Core 3.1 Web App calling an ASP.NET Core 3.1 Web API, which in turn accesses an Azure SQL database. Authentication is provided via MSAL (Microsoft Identity Platform) - i.e. using the relatively new Microsoft.Identity.Web and Microsoft.Identity.Web.UI libraries.
The goal is to ensure that the user pulls data from SQL via the API under the context of his/her own login, thus enabling row-level security, access auditing and other good things.
I have succeeded in getting the sign-in process to work for the Web App - and through that it obtains a valid access token to access the API using a scope I created when registering the latter with AD.
When I run both the API and the App locally from Visual Studio everything works as expected - the correct access tokens are provided to the App to access the API, and the API to access SQL - in both cases under the user's (i.e. my) identity.
When I publish the API to App Services on Azure, however, and access it there either from a local version of the Web App or an App-Services hosted version of it, the access token that the API gets to access SQL contains the API's Application Identity (system-assigned managed identity), and not the user's identity. Although I can access SQL as the application, it's not what we need.
The Web App obtains its access token using the GetAccessTokenForUserAsync method of ITokenAcquisition - taking as a parameter the single scope I defined for the API.
The API gets its token (to access SQL) like so:
var token = await new AzureServiceTokenProvider().GetAccessTokenAsync("https://database.windows.net", _tenantId)
...where _tenantId is the tenant ID of the subscription.
I have added the SQL Azure Database "user_impersonation" API permission to the AD registration for the API - but that has not helped. As an aside, for some reason Azure gives the full name of this permission as https://sql.azuresynapse.usgovcloudapi.net/user_impersonation - which is slightly alarming as this is just a UK-based regular Azure account.
I have found a few similar posts to this, but mostly for older versions of the solution set. I'm hoping to avoid having to write my own code to post the token requests - this is supposed to be handled by the MSAL libraries.
Should I somehow be separately requesting a SQL access scope from the Web App after sign-in, or should the API be doing something different to get hold of a SQL access token that identifies the user? Why does it work perfectly when running locally?
It seems like this should be a very common use case (the most common?) but it is barely documented - most documentation I've found refers only to the application identity being used or doesn't tell you what to do for this particular tech stack.
Finally - success! In the end this was the critical piece of documentation: Microsoft identity platform and OAuth 2.0 On-Behalf-Of flow - the key points being:
The App only asks for a token to access the API.
The API then requests a token, on behalf of the user identified via the 1st token, to access SQL.
The key is that - since the API cannot trigger a consent window for the second step - I had to use the Enterprise Applications tab in the Azure portal to pre-grant the permissions for SQL.
So the good news is it does work: maybe it's obvious to some but IMO it took me far too long to find the answer to this. I will write up a fuller explanation of how to do this in due course as it can't only be me struggling with this one.
The bad news is that - in the course of my investigations - I found that Azure B2C (which is the next thing I need to add in) doesn't support this "On Behalf Of" flow - click here for details. That's a great shame as I think it's the most obvious use case for it! Oh well, back to the drawing board.
I'm currently working on a similar problem, using a Net5.0 Web app. The reason it appears to be working locally is you are signed into Visual Studio with a user who can access Azure SQL and those are the rights you get in the Db. The IDE is using those credentials in place of the Managed Service Identity, the latter gets used when you upload the app to Azure.
As you noted, in the App registration you need to grant permission to the App for Azure SQL Database user_impersonation.
In your code, you need to request a token from https://database.windows.net//.default (note the // as it's needed for v1 endpoints). By referencing /.default you are asking for all permissions you've selected for the app in the app registration portal.
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope
In Startup.cs you need to EnableTokenAcquisitionToCallDownstreamApi with the scope you require.
services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(new[]
{"https://database.windows.net//.default"})
// Adds the User and App InMemory Token Cache
.AddInMemoryTokenCaches();
services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the
// default policy
options.FallbackPolicy = options.DefaultPolicy;
});
services.AddDbContext<MyDatabaseContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("MyAzureConnection")));
// The database interface
services.AddScoped<ITodos, TodoData>();
services.AddRazorPages()
.AddRazorRuntimeCompilation()
.AddMvcOptions(o =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
o.Filters.Add(new AuthorizeFilter(policy));
})
.AddMicrosoftIdentityUI();
You also need to decorate your controllers with [AuthorizeForScopes(Scopes = new string[]{"https://database.windows.net//.default"}] and include the required scopes for that Controller. For Razor, it's at the top of the page model and requires a reference to `using Microsoft.Identity.Web;'
namespace ToDoApp.Pages.Todos
{
[AuthorizeForScopes(ScopeKeySection = "AzureSQL:BaseUrl")]
public class CreateModel : PageModel
I'm using a section in my appsettings.json for the scope and retrieving it using ScopeKeySection:
"AzureSQL": {
"BaseUrl": "https://database.windows.net//.default",
"Scopes": "user_impersonation"
}
This shows you where to include it for MVC, Razor and Blazor:
https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access#in-mvc-controllers
Finally, your DbContext needs a token which you could pass to it from the client app (perhaps...).
This is how I am doing it at the moment
public class MyDatabaseContext : DbContext
{
private readonly ITokenAcquisition _tokenAcquisition;
public MyDatabaseContext (ITokenAcquisition tokenAcquisition,
DbContextOptions<MyDatabaseContext> options)
: base(options)
{
_tokenAcquisition = tokenAcquisition;
string[] scopes = new[]{"https://database.windows.net//.default"};
var result = _tokenAcquisition.GetAuthenticationResultForUserAsync(scopes)
.GetAwaiter()
.GetResult();
token = result.AccessToken;
var connection = (SqlConnection)Database.GetDbConnection();
connection.AccessToken = result.token;
}
This is a flawed solution. If I restart the app and try to access it again I get an error Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user
It seems to be related to the TokenCache. If I sign out and in again or clear my browser cache the error is resolved. I've a workaround that signs the app in on failure, but it's deficient since I'm using the app's credentials.
However, it successfully connects to the Azure SQL Db as the user instead of the App with the user's rights instead. When I do solve the error (or find one) I will update this answer.

identity server multiple issues after deployment

My current setup is like this. The entire project was built using the official docs here - https://identityserver4.readthedocs.io/en/latest/
API Server
Auth Server with local login, google login and github login
Console based c# client
JS based client
MVC based client.
(all of it, as described in the official docs)
Locally, all of them work beautifully. Able to login, access api endpoints, logout, redirect, the whole thing works smooth.
I have deployed all 5 of them to five different azure web apps. They all have the standard xyz.azurewebsites.net domains ready to use. Now, I have run into some problems.
the console C# client is able to talk to the deployed auth server, collect token using a local account on the auth server and make calls to the deployed API server. Based on this, I assume that both the api server and the auth server working hand in hand, as they should.
Problem #1 - the JS client keeps saying
'The login is blocked because of CORS Missing Allow Origin '
Problem #2 - the MVC client loads the auth server, and then the auth server gives me this error.
Sorry, there was an error : unauthorized_client
Request Id: 80005c0f-0000-eb00-b63f-84710c7967bb
Note : I have set the CORS policy on the auth server, both these clients, under client definition as follows. I am not too concerned about keeping the auth server open, so dont mind if any and every domain can call the auth server.
AllowedCorsOrigins = { "*.*" },
Also Note : I have set the URLS in the code before deployment. all loclahost:port number lines have been replaced correctly with the corresponding now published URLs.
So, what am I missing out here?
Update 1
I was able to solve the CORS issue. Have posted a answer here on another question.
Not able to enable CORS for identity server 4 in asp.net core
Update 2
So, now, both the JS client and the MVC client, are giving identical errors.
Sorry, there was an error : unauthorized_client
Request Id: 80005c0f-0000-eb00-b63f-84710c7967bb
Update 3
I have opened an issue which has log details.
https://github.com/IdentityServer/IdentityServer4/issues/4691
I am not sure if this counts as an answer, but posting for my own question, as it might might help others. Also, this is only a guess at this point.
I found out that the redirects were permanently stored in the database I used with EF migrations. That mean, local in memory redirects were being overwritten anyway by the database stored migrations. I believe this is the issue.
I also realized that the console app is working fine for it does not depend on redirect URLs where as the JS and MVC based clients dont work because they do depend on redirect URLs.
At this point, the best thing to do and for you (if you used EF migrations to store your auth server configuration) on database would be start over and switch to in memory only. Alternatively, you can try and update the database to suit your deployment requirements.
Ultimately, I believe, unless it is absolutely necessary, keep the auth server config (like redirects and CORS settings) in memory as they dont take up much value and are rarely changed.

What would be the impact of changing or setting the SignInScheme when already deployed in production

Our authentication system is currently seeing many "Correlation failed" errors for some users trying to connect via Google Sign In. We're using Identity Server 4 / ASP.NET identity with .NET Core 2.2. Obviously, we can't reproduce.
Though we tried to understand every aspect of what we were implementing and deploying, the whole authentication flow and the many configuration possibilities of ASP.NET Identity and IS4 still hold mystery to us. We've tried to pinpoint the true cause of those "correlation failed" errors, but cannot be sure. We do have data protection in place that prevents issues with load balancing and multiple instances. We're almost certain it's not related to HTTP vs HTTPS. But foremost, it looks like this error is more frequent lately, though we don't see what change could have triggered this.
One thing we realized, and we're not sure of the impact, is that the Google authentication is configured without an explicit SignInScheme:
services.AddAuthentication()
.AddGoogle(options =>
{
var section = Configuration.GetSection("ExternalLogin:Google");
options.ClientId = section.GetValue<string>("ClientId");
options.ClientSecret = section.GetValue<string>("ClientSecret");
});
Documentation about IS4 seems to indicate we should have set this in the first place:
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
On the other hand, articles like this one say that this scheme gets replaced by ASP.NET Identity.
Because ASP.NET Identity also changes the default authentication scheme, any instances of IdentityServerConstants.DefaultCookieAuthenticationScheme and IdentityServerConstants.ExternalCookieAuthenticationScheme, should be changed to IdentityConstants.Application and IdentityConstants.ExternalScheme respectively.
This just adds to our overall confusion, and we're not sure if it means changes to the external login callback as well.
Can we add that simple SignInScheme line to the Google options without impacting other users which still can connect with Google Sign In without any issue? Does it have a chance to fix the correlation errors we see? Otherwise, where should we look?

Azure App Service Authentication - Pulled data even when authentication failed

I have a Xamarin Forms application that is targeting Windows UWP. I have successfully setup Offline Sync and Authentication by following the Microsoft docs such as this article: https://learn.microsoft.com/en-us/azure/app-service-mobile/app-service-mobile-windows-store-dotnet-get-started-users
I have also successfully setup a background (out-of-process) task to run using a Windows Universal Runtime Component.
The process flows as:
1) User runs the application for the first time, they must login and authenticate to Active Directory. The application then caches their token and registers the background task.
2) The background task kicks off on a Timer. During testing, I did NOT have it refreshing the token because I wanted to test the 1-hour token expiration and refresh process.
In my testing, I discovered two things that I am not completely understanding and which I found odd (not behaving as I expected).
1) When I examine the expiration date of the token the user receives upon first login, the expiration date is set for 30 days in the future. I thought it was supposed to expire after 1 hour. Why does it show 30 day expiration date? (This isn't too big of a concern as I did confirm that it does stop allowing access after an hour, just an oddity that I am observing).
2) This is the one I have a concern about. Even though the background task was not able to authenticate after the 1-hour period, it still was able to pull data from Azure to the device. The push data sync failed because authentication failed, but the pull data sync succeeded. This is concerning because I do not want unauthorized attempts to be able to pull data. Has anyone else ran into this? Is this a bug? Did I configure authentication incorrectly somehow?
My Settings:
Azure App Service > Easy Tables (Node.js backend) has all permissions set to "Authenticated Access only".
Azure App Service > Settings > Authentication/Authorization >
App Service Authentication is set to On,
Action to take when request is not authenticated is set to Log in with Azure Active Directory,
Authentication Providers is set Configured for Azure Active Directory.
Mobile App Code:
MobileServiceClient client;
IMobileServiceSyncTable<TableToPull> tableToPullDataFrom;
IMobileServiceSyncTable<TableToPush> tableToPushDataTo;
this.client = new MobileServiceClient(Constants.ApplicationURL);
this.client.CurrentUser = new MobileServiceUser(Settings.UserId);
this.client.CurrentUser.MobileServiceAuthenticationToken = Settings.AuthToken;
store.DefineTable<TableToPull>();
store.DefineTable<TableToPush>();
The Push Async Code is (which fails when authentication fails, as expected):
await this.client.SyncContext.PushAsync();
The Pull Async Code, which should fail when authentication fails but did not (this is my concern) is:
await this.tableToPullDataFrom.PullAsync("tableQuery",this.tableToPullDataFrom.CreateQuery());
I am concerned that the background task was able to pull data from Azure even though authentication failed. I am hopeful that I am misunderstanding this concept in some way and someone can explain to me why this happens? Or if I need to configure something differently? I need to make sure unauthorized logins cannot pull data.
The background task attempts to push and pull data in sequence and I can see that it fails due to invalid authentication during the push task but then succeeds during the pull task. I can also verify this because the pull task does grab updated data from the SQL Azure DB (I can view it using Isolated Storage Explorer and SQLite Browser) but it doesn't send changed data to the SQL Azure DB (viewed in SSMS).
The "action to take when authentication fails" is wrong. You need to set it to "No action". This is so that the authentication can be handled by your code.
If you are using Easy Tables, adjust the permissions on the table to "Authenticated". If you are using ASP.NET, add the [Authorize] attribute to your class.
Authentication is covered in depth in my book - chapter 2 of http://aka.ms/zumobook.