.Net Core 3.1 - Google Auth Cookie Not Persisting - c#

I recently upgraded my ASP.Net Core MVC web app to .Net Core 3.1, and ever since, the Google External login cookie hasn't persisted properly as it did before the upgrade. The code itself didn't change at all, the app was just upgraded from .Net Core 2.0 to .Net Core 3.1.
The login flow works (user clicks login button, Google login screen appears, user logs in with Google Credentials/selects existing Google user and is sent back to the web app), but at random times (sometimes 30-60 seconds after login), the user's session randomly ends and the user is redirected back to the login screen. I've tested this extensively on localhost and the issue does not occur, but happens frequently on the hosting service I'm using (MochaHost).
Has anyone else had this issue with .Net Core 3.1?
The ConfigureServices method used at startup is below for reference.
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging();
services.AddResponseCompression(options =>
{
options.Providers.Add<GzipCompressionProvider>();
options.EnableForHttps = true;
});
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(Configuration["ConnectionStrings:MySQL"]));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddUserStore<UserStore>()
.AddDefaultTokenProviders();
services.AddAuthentication().AddGoogle(options =>
{
options.ClientId = Configuration["Authentication:Google:ClientId"];
options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
options.CallbackPath = new PathString("/signin-google");
});
services.ConfigureApplicationCookie(options =>
{
options.AccessDeniedPath = "/error/401";
options.Cookie.Name = "MyApp";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(180);
options.LoginPath = "/login";
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.SlidingExpiration = true;
});
services.AddMvc(option => option.EnableEndpointRouting = false);
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IFileProvider>(new PhysicalFileProvider(Configuration.GetValue<string>("RootFilePath")));
var serviceAccountFilePath = GetGoogleServiceAccountCredentialPath();
var googleCredential = GoogleCredential.FromFile(serviceAccountFilePath);
services.AddSingleton(StorageClient.Create(googleCredential));
}

In my case, the issue was that Data Protection key storage was not available via user profile or HKLM registry after the .Net Core upgrade. I'm not sure if this is related to having to switch to 64-bit app pool mode after the upgrade to .Net Core 3.1 or not, but the bottom line is data protection keys were only being stored in memory. After shifting from server to server via the load balancer, the user keys were lost, causing a redirect back to the login page.
Sample Log entries:
2020-11-20 09:00:21.162 -08:00 [INF] Starting web host
2020-11-20 09:00:21.572 -08:00 [WRN] Using an in-memory repository. Keys will not be persisted to storage.
2020-11-20 09:00:21.574 -08:00 [WRN] Neither user profile nor HKLM registry available. Using an ephemeral key repository. Protected data will be unavailable when application exits.
I had to add the below code to use a folder on the hosting service to store key files so the user's session would persist:
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(Configuration.GetValue<string>("KeyStorePath")));

Related

.Net Core Identity AuthenticationCookie not working

I recently ran into a problem with my net5.0 application (upgraded from net core 3.1). I am using Microsoft.AspNetCore.Identity to sign in and it works perfectly fine. After deploying it to the production machine the login is still working and I receive my cookie. But after calling the url on the next day I am not logged in despite having my auth cookie (it's valid for 30 days). On my local machine hosted on IIS it works fine. Here are parts of my code:
Startup.cs:
ConfigureServices
services.ConfigureApplicationCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
options.LoginPath = new PathString("/Account/Login");
options.LogoutPath = new PathString("/Account/Logoff");
options.Cookie.Name = "MyApplication";
options.Cookie.IsEssential = true;
});
I am using the ProtectPersonalData attribute to encrypt userdata in the database with a keyring:
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.Stores.ProtectPersonalData = true;
config.Stores.MaxLengthForKeys = 128;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
Configure
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
And this is the cookie in Firefox:
[cookie][1]
In my understanding this cookie should be send to the server on every request for that domain. And even though Firefox states, that it was accessed last on the exact time of my request it has no effect.
If you need any further code let me know. Any help is appreciated!
[1]: https://i.stack.imgur.com/UTSuR.png
Seems like everything was ok with my code. The error was within the IIS Configuration. As I am using DataProtection the ApplicationPool should have "load userprofile" set to true, because otherwise it can not store the keyring and data is lost after application shutdown/restart. After changing that on the production server it seems to be working now.

ASP.NET Core MVC Azure AD Authentication Loop on Azure App Service

I have an ASP.NET Core MVC application and I integrated Azure AD into it using the following code:
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = true;
});
The Azure AD Configuration contains these properties:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]",
"TenantId": "<tenant-id>",
"ClientId": "<client-id>",
"CallbackPath": "/signin-oidc"
},
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "user.read User.ReadBasic.All"
}
It all worked locally and when deployed to an Azure App Service we did not have any problems.
I needed to integrate Microsoft Graph for a new module in the Web App (I need to look up the users in specific groups of the Azure AD).
I followed the Microsoft Graph Tutorial to implement Microsoft Graph.
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
// Handling SameSite cookie according to https://learn.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
options.HandleSameSiteCookieCompatibility();
});
// Sign-in users with the Microsoft identity platform
string[] initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
It all works locally but as soon as the app is deployed into an Azure App Service then app goes into a redirect loop when the user is authenticated.
I reversed the changes to the original code and it gets fixed but then I can't use Microsoft Graph because I do not have the access token.
I checked all the redirect urls in the App Registration and they seem to be fine. I even included
https://<app-name>.azurewebsites.net/
in addition to
https://<app-name>.azurewebsites.net/sigin-oidc
just to be sure it wasn't a redirect url issue.
I thought it was the Katana bug but that seems to have been fixed in .NET Core.
I enabled HTTPS Only on the Azure App Service but the issue still persists.
Has anyone experienced something like this issue? I've been at it for a couple of days and I can't solve it.
Any helped is greatly appreciated.
I tried publishing the microsoft tutorial linked in the question to a new Azure App Service with a new App Registration in Azure AD.
It signed in without a problem.
After that I suspected that there might be a problem with the App Registration. I came to that conclusion because the Azure AD Registration that the MVC Web App was using was created directly in Visual Studio and not manually in the Azure Portal.
Finally, all I did was create a new App Registration in Azure AD and published the MVC Web App with the new client secret and client id. Everything seem to work fine on the published Azure App Service.

Register external users using MSAL with Identity on ASP.NET Core

Problem:
In my ASP.NET Core 3.1 MVC Web app I Use the services.AddSignIn(Configuration); in my startup.cs (provided by the Microsoft.Identity.Web 0.1.5-preview) and I want to register a User upon logging in, the callback method ExternalLoginInfo info = await _signInManager.GetExternalLoginInfoAsync(); always returns null.
Background:
I have an application which uses ASP.NET
Identity
to register and store users. - all the users use their Microsoft
accounts
I wanted to enhance the app to use MS Graph API (to reach a Sharepoint
site for some files), so I've modified the app to use the
MSAL
for authentication against the Graph API.
I've used these MSAL
tutorials
to try the features out and the sign in went well, I could connect
the graph API as well.
Details:
My appsettings.json:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "something.onmicrosoft.com",
"TenantId": "GUID",
"ClientId": "GUID",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath ": "/signout-callback-oidc",
"ClientSecret": "GUID"
}
Startup cs:
services.AddDbContext<UserDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection"), u => u.MigrationsAssembly("DatabaseLayer.Core")));
services.AddIdentity<AppUser, IdentityRole>(options =>
{
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<UserDbContext>()
.AddDefaultTokenProviders();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
// Handling SameSite cookie according to https://learn.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
options.HandleSameSiteCookieCompatibility();
});
services.AddOptions();
services.AddSignIn(Configuration);
Question: What do I need to configure to get any externallogins returned in order to register the users?
According to this, it is not possible to catch external users using MSAL. Moreover it is not possible to use any internal users as well, because they would interfere with the logged in MSAL user.
So Microsoft's ASP.NET Core Identity framework is incompatible with their identity framework, nice. (Pity, this makes MSAL useless for me, and I cannot think of any business case where a productive application does not have to have any internal repository of users to work properly. Even if I only would need to store a user's setting I would use the identity framework for this purpose).

Server-side Blazor users logged out when signalR circuit ungracefully broken

I have an asp.net core 3.1 server-side Blazor web site that uses the Microsoft Identity framework for authentication. It appears that whenever a user's SignalR circuit is ungracefully broken they lose their authentication state and have to log in again.
In these scenarios the .AspNetCore.Identity.Application cookie will still be present and unexpired. The problem doesn't occur if the circuit terminates gracefully (e.g. closing the browser); it's only in situations like a vpn disconnecting.
Here is the authentication section of Startup:
services.AddDbContext<IdentityDataContext>(options => options.UseSqlServer(Configuration.GetConnectionString("IntegrationDatabase")));
services.AddDefaultIdentity<IdentityUser>(options =>
{
options.Password.RequiredLength = 10;
options.SignIn.RequireConfirmedAccount = false;
options.SignIn.RequireConfirmedEmail = false;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<IdentityDataContext>();
services.AddAuthorization(options =>
{
options.AddPolicy(CanViewDataPolicy.Name, CanViewDataPolicy.Policy);
options.AddPolicy(RequireAdminPolicy.Name, RequireAdminPolicy.Policy);
});
Does anyone know why this occurs or how to prevent it? I would like users to remain authenticated until they log out or their cookie expires.

How to deploy to Azure an Asp.Net-Core 2.2 Razor Pages App with AzureAD Authentication

I'm creating a new WebApp using ASP.NET Core 2.2 and Razor Pages and I want to authenticate users using AzureAD. This works fine locally with localhost and I can sign in and out with no problem. But when I publish it to azure I can not sign in. After the microsoft sign in pages I am redirected to my WebApp to the page "/.auth/login/done" that says: "You have successfully signed in. Return to the website" and I can return to my website but I am not logged in.
In the App Registration in Azure I have configured the redirect Urls for localhost and for the application. For localhost it looks like "https://localhost:44321/.auth/login/aad/callback" and for the app something like "https://maywebapp.azurewebsites.net/.auth/login/aad/callback".
I configured the App to use always https to make sure that the Url is the same as the configured in Azure.
This is my Service Configuration:
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(options =>
{
options.Authority = Configuration["Microsoft:Authority"];
options.ClientId = Configuration["Microsoft:ClientId"];
options.CallbackPath = Configuration["Microsoft:CallbackPath"];
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.SignedOutRedirectUri = Configuration["Microsoft:SignedOutRedirectUri"];
options.TokenValidationParameters.NameClaimType = "name";
})
.AddCookie();
I expect the same behaviour as when I am running the working App locally.
To make all accesses to pages in this folder be authenticated, we could add the Authorize attribute to all page model classes, but we can do better than that. Going back to the Startup class, we can add some Razor Pages conventions to make our lives easier.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/Account");
});
...
}
Here is an article about authenticate with asp.net razor.

Categories