Suspected bug in Microsoft Identity Platform with ASP.NET Core 3.1 Razor Pages - asp.net-core

I am developing an application to be hosted in the Azure App Services environment which consists of a front-end Web App, a back-end Web API and a SQL Database (using Azure SQL). The front-end Web App is a Razor Pages app. We are trying to use the Microsoft Identity Platform (via Microsoft.Identity.Web and Microsoft.Identity.Web.UI libraries) to acquire an access token for the API when needed.
It works perfectly well the first time, but once a token has been acquired and cached - if the application is restarted it fails with this error:
IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent.
No account or login hint was passed to the AcquireTokenSilent call.
Startup configuration is (I've tried various variants of this):
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
options.HandleSameSiteCookieCompatibility();
});
services.AddOptions();
services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { Configuration["Api:Scopes"] })
.AddInMemoryTokenCaches();
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
services.AddRazorPages().AddRazorRuntimeCompilation().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
services.AddMvc();
//Other stuff...
}
I have tried for many days trying to find either a resolution workaround for this. I can catch the
error, but there is no action we can take programmatically that seems to clear the problem (the ITokenAcquisition interface does not offer the option to force an interactive login).
I have found that it is ONLY a problem in a Razor Pages application - a controller-based MVC Web App with almost identical startup code does not exhibit the problem.
I have also found that, by creating a controller-based test MVC Web App and configuring it with the same client id, tenant id etc. as the app we're having problems with, then starting it up (within the Visual Studio development environment) as soon as the main app gets the problem, I can clear the error condition reliably every time. However this is obviously not a viable long-term solution.
I have searched for this problem on every major technical forum and seen a number of similar sorts of issues raised, but none provides a solution to this precise problem.
To replicate:
Create an ASP.NET Core 3.1 Web API.
Create an ASP.NET Core 3.1 Razor Pages Web App that calls the API.
Register both with Azure Active Directory and configure the App to request a token to access the API (as per various MS documents).
Run - if everything is set up correctly the login screen will appear and all will work correctly.
Stop the Web App, wait a couple of minutes and re-start. The error above will now appear.
I have raised a Microsoft support request for it - has anybody else come across this and found a solution for it?

I have finally got to the bottom of this, largely thanks to this: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/issues/216#issuecomment-560150172
To summarise - for anyone else having this issue:
On the first invocation of the web app you are not signed in, and so get redirected to the Microsoft Identity Platform login, which logs you in and issues an access token.
The access token is stored in the In-Memory token cache through the callback.
All then works as expected because the token is in the cache.
When you stop, and then re-start the web app within a reasonably short time, it uses the authentication cookies to pick up the still-current login, and so it does not access the Identity Platform and you do NOT get an access token.
When you ask for a token the cache is empty - so it throws the MsalUiRequiredException.
What isn't really made clear in any of the documentation is that this is supposed to happen - and that exception is picked up by the "AuthorizeForScopes" attribute but only if you allow the exception to fall all the way through and don't try to handle it.
The other issue is that in a Razor Pages app the normal AuthorizeForScopes attribute has to go above the model class definition for every page - and if you miss one it may trigger the above problem.
The solution proposed by "jasonshave" in the linked article solves that problem by replacing the attribute with a filter - so it will apply to all pages.
Maybe I'm a bit old-school, but the idea of using an unhandled exception as part of a planned program control flow doesn't sit right with me - at the very least it should be made clear that that's the intention. Anyway - problem now solved.

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

How to republish an ASP.NET Core 5 web application in IIS. Changes not taking effect

I've published an ASP.NET Core 5 Web API to IIS (using VS 2019 webdeploy), and it is running fine. Now I've made some minor changes and republished.
The changes take affect on my local machine using IIS Express, but I do not see them when I publish to my IIS server. I can see that the exe and dlls are updated, but the webapi behaviour does change.
Something as simple as setting the swaggerdoc title doesn't take affect.
Is there something else I need to do to refresh the app? I've tried recycling the app pool and doing an iisreset, but no luck. I've read that IIS is merely acting as a reverse proxy to Kestrel which is actually hosting the api. If so how do I recycle that?
As a test, I delete all the files in my web api directory. I browse to the web api and get the following error:
HTTP Error 500.31 - Failed to load ASP.NET Core runtime Common
solutions to this issue: The specified version of
Microsoft.NetCore.App or Microsoft.AspNetCore.App was not found.
Troubleshooting steps:
Check the system event log for error messages
Enable logging the application process' stdout messages Attach a
debugger to the application process and inspect For more information
visit: https://go.microsoft.com/fwlink/?LinkID=2028526
I then re-publish the web api and browse to it, but it still has the old behavior. I'm really confused, because I know the published dll for my service is correct.
If I use ILSpy I can see the following code:
public void ConfigureServices(IServiceCollection services)
{
IConfigurationSection appSettingsSection = Configuration.GetSection("ApplicationSettings");
ApplicationSettings appSettings = appSettingsSection.Get<ApplicationSettings>();
services.AddSingleton(appSettingsSection.Get<ApplicationSettings>());
services.AddControllers()
.AddXmlSerializerFormatters()
.AddJsonOptions(delegate(JsonOptions o)
{
o.JsonSerializerOptions.PropertyNamingPolicy = null;
o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
services.AddSwaggerGen(delegate(SwaggerGenOptions c)
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "My Demo Service (" + appSettings.Environment + ")",
Version = "v1"
});
});
}
When I browse to the swagger page of my API on my local dev box the title matches code, and the property naming is what I want. When I browse to it on the server, the title of the swagger page is the old value, and the property naming policy is camel case, so I'm pretty sure I'm publishing the new version of my web api, but it's not being executed when I make http requests to the api. Something must be cached somewhere.

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.

Sign out not working if deployed to IIS as a web application

Good morning. We have an asp .net core 2.1 with Angular application.
We use asp.net identity.
To logout users we use await HttpContext.SignOutAsync();
It works fine running the application in visual studio and deploying to a root website, but if we deploy to a webapplication inside a website in IIS, the logout doesn't work at all.
I assumed that the SignOutAsync deleted all information server side so even if the cookies were not modified, the server would reject any future request, but it seems it isn't.
Any idea?
It seems solved.
Using await _signInManager.SignOutAsync(); works fine.
I don't know the difference regarding using HttpContext.SignOutAsync()
You are using the Asp.net Core Identity ,so the authentication ticket in inside in .AspNetCore.Identity.Application cookie , you can try to delete the cookie by set specific scheme :
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
But use SignInManager.SignOutAsync() is better which will delete below cookies :
await Context.SignOutAsync(IdentityConstants.ApplicationScheme);
await Context.SignOutAsync(IdentityConstants.ExternalScheme);
await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme);

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?