Issue
In my ASP.NET MVC Core 2.0 application I have setup to use the cookie authentication scheme without using Identity as we have our own backend authentication storage and api.
The authentication and authorization works perfectly every time.
However, no matter what the login/session expires after approximately 30 minutes. You can see that we are setting the timeout to 120 minutes for both authentication cookie and session cookie.
Application information:
Platform: .Net 4.7.x (Windows)
Framework: Asp.Net Core 2.x
IIS used as a proxy server
Any help or input on how to solve this would be welcome
Code
Update: Replaced services.AddMemoryCache() with services.AddDistributedRedisCache(..) - testing to see how this works
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedRedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = "CoreTestInstance";
});
services.AddAuthentication("CookieAuthenticationScheme")
.AddCookie("CookieAuthenticationScheme", options =>
{
options.Cookie.Name = authSettings.Name;
options.Cookie.HttpOnly = false;
options.Cookie.Expiration = TimeSpan.FromMinutes(120);
options.ExpireTimeSpan = TimeSpan.FromMinutes(120);
options.AccessDeniedPath = new PathString("/Errors/StatusCodeErrors/401");
options.LoginPath = "/Account/Login";
});
// services.AddMemoryCache();
services.AddSession(options =>
{
options.Cookie.Name = sessSettings.Name;
options.Cookie.HttpOnly = false;
options.IdleTimeout = TimeSpan.FromMinutes(120);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Errors/Default");
}
app.UseStatusCodePagesWithRedirects("/Errors/StatusCodeErrors/{0}");
app.UseStaticFiles();
app.UseAuthentication();
app.UseSession();
app.UseMvc();
}
AccountController.cs
[HttpPost("Login")]
public async Task<IActionResult> Login(AccountModel model)
{
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, model.UserName));
claims.Add(new Claim(ClaimTypes.Role, "Administrator", ClaimValueTypes.String, model.UserName));
var identity = new ClaimsIdentity(claims, "login");
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("CookieAuthenticationScheme", principal);
}
You're using in-memory sessions, which are tied to the process. That process in IIS is your App Pool. By default, the App Pool recycles automatically after a period of time. When it recycles, it takes your sessions with it.
Use a persistent session store: SQL Server, Redis, etc. (Sessions use distributed cache, so the way you set up persistent sessions is to setup a persistent distributed cache store.)
Thanks to Chris Pratt to starting me in the right direction. Moving to Redis helped with performance and distributed caching is the right way to go.
However, Redis did not solve my problem, the app still timed out after 90 minutes regardless of my settings.
Since we use IIS I ended up having to change settings in the AppPool to finally get the session to honor my timeout instead of the AppPool's.
For the AppPool, click on the Advanced Settings and make the following changes:
Section: Process Model
Idle Time-out (minutes) = 0 (disables the timeout so that the application pool will never shut down due to being idle)
Idle Time-out Action = Suspend (A suspended worker process remains alive but is paged-out to disk, reducing the system resources it consumes)
Section: Recycling
Regular Time Interval (minutes) = 0 (setting to 0 means the apppool will not recycle)
Note: The above settings should not affect other applications unless you have a great deal many applications running on the same server. I suggest you research the settings in detail before using them to ensure they will work for you.
Finally, I ran several tests with various settings (we store them in appsettings.json) for timeout from 2 mins, 10 mins, 1 hour and finally 2 hours and all worked as required.
Related
I am trying to set the session timeout to 30 mins in .NET 6.
I added the following code for it.
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
options.IdleTimeout = TimeSpan.FromMinutes(30));
It is not working and the session is expiring in a few minutes.
When session is expired, the server will delete all session variables set on login and there is no way to see if it has been expired from client side (one of the many reasons on why I switched from Sessions to JsonWebTokens).
To use session, add following code in your Program.cs
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
// make the session cookie Essential
// so that session variable is not null
// check this StackOverflow answer: https://stackoverflow.com/a/64984796/19112855
options.Cookie.IsEssential = true;
});
app.UseSession(); // before app.UseAuthentication(); and after app.UseRouting();
To manually delete all session variables, call this in your controller HttpContext.Session.Clear();
I am having some weird issues with authentication in my app. To preface, it is a C#, ASP.NET Core 6 MVC web app. Users log in and all controllers have an [Authorize] tag.
I am making use of sessions to determine what menu options to show the users and this is working as far as I can tell when the session timer ends. My issue is with the users that are logged in, I'm not using anything special, just the built in Identity provider reading from the SQL Server.
What happens is the authentication never times out even after I stop running the application and start it again, the previously logged in user is displayed as being valid as opposed to making me log in again. I have looked at a number of other questions and articles and based on what I've been seeing I have added the following code to the Program.cs file.
For the basis of testing I made the idle timeouts just 2 minutes for both authentication and sessions. My session does expire after 2 minutes but the authentication never expires.
Can anyone point me in the right direction or tell me what I'm doing wrong?
Thanks!
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(2);
options.AccessDeniedPath = "/Home/Index/";
options.LoginPath = "/Home/Index/";
options.LogoutPath = "/Home/Index/";
options.Cookie.Expiration = TimeSpan.FromMinutes(2);
options.Cookie.Name = ".TOR";
});
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(2);
options.Cookie.Name = ".TORSession";
options.IOTimeout = TimeSpan.FromMinutes(2);
});
I have logged in manually to a .NET Core 3 Web API app as seen in the controller action below. No matter what I try, I can't seem to get the .NET framework to recognize that the application user is logged in. I am testing locally in Visual Studio, but this behavior is also reflected when I test on a server as well. Why isn't the principal that I have created and used in the HttpContext.SignInAsync method being sent to the middleware (at least it seems as though it is not)?
Note: I have seem some posts where users say you have to sign in and then make another request to the server for the new principal to take effect. I have tried this but the result is the same.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//........
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = DefaultAuthenticationTypes.ApplicationCookie;
})
.AddCookie(DefaultAuthenticationTypes.ApplicationCookie, options =>
{
// true by default
options.SlidingExpiration = true;
// 14 days by default
options.ExpireTimeSpan = TimeSpan.FromMinutes(120);
options.Cookie.IsEssential = true;
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.None;
options.LoginPath = "/Login";
options.LogoutPath = "/Logout";
});
services.AddAuthorization();
//........
}
public void Configure(IApplicationBuilder app)
{
//........
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseCors();
//........
}
Controller Action:
ClaimsIdentity userIdentity =
new ClaimsIdentity(jwtSecurityToken.Claims, DefaultAuthenticationTypes.ApplicationCookie);
ClaimsPrincipal principal =
new ClaimsPrincipal(userIdentity);
await HttpContext.SignInAsync(DefaultAuthenticationTypes.ApplicationCookie, principal,
new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddDays(7)
});
AuthenticateResult authenticateResult =
await httpContext.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie);
//This always returns false!!!
User.Identity.IsAuthenticated
//This always returns false!!!
I hate to do this, but this code actually works. If you hit this code via the browser directly, the code logs you in as expected and it all works as expected.
What I was trying to do was get it to work via a Vue.js front end app using a .NET Core 3 Web API 2 app on the backend. For some reason, the Vue app cannot log the backend app in. When on a Vue front page and I hit the API action to login via Vue code, the .NET code logs in with no errors, but on any other call from the VUE app, the .NET app says that the user is unauthorized. I suspect that there is something going on with how the cookie is stored and data privacy around the cookie.
In the context of this question, this code works as expected. Thank you.
I'm hosting an asp.net core application (admin) behind an IIS server configured as a reverse proxy. The app uses open id connect to authenticate users against a separate application (core) which uses Identity Server 4 (also hosted behind the same reverse proxy). Both apps are using .NET Core 3.1. The reverse proxy is configured to forward requests for http://192.168.0.161/admin to the admin app which is hosted on the same machine under http://localhost:44321. This works so far until a user tries to login. The user gets correctly redirected to the core apps login page but this redirect does contain the wrong signin_oidc redirect uri to redirect back to the admin app. The signin_oidc redirect uri gets always set to http://localhost:44321/signin-oidc instead of http://192.168.0.161/admin/signin-oidc.
I already searched around the internet but every solution I found tells me to configure the ForwardedHeadersOptions and call app.UseForwardedHeaders() before app.UseAuthentication() and everything else should be handled automatically. But it doesn't.
It seems like the application does not take those headers into account.
Code for admin app:
Startup/ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.Authority = openIdConnectAuthority;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = OidcConstants.ResponseTypes.Code;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = false;
});
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
...
}
Startup/Configure:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseForwardedHeaders();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute().RequireAuthorization();
endpoints.MapRazorPages();
});
}
When I debug the application these are the relevant request headers which are sent to the app:
{[X-Forwarded-Host, {192.168.0.161}]}
{[X-Forwarded-Proto, {http}]}
{[X-Original-URL, {/admin}]}
{[X-Forwarded-For, {192.168.0.161:51920}]}
What am I doing wrong? How can I tell the open id connect middleware to take those headers into account or at least hard code the signin_oidc return url? Because setting the CallbackPath for oidc does only allow relative urls, so no chance of changing the base path.
Edit
I also tried overriding the OnRedirectToIdentityProvider event in AddOpenIdConnect() and setting the return url manually like this:
options.Events.OnRedirectToIdentityProvider = ctx =>
{
ctx.ProtocolMessage.RedirectUri = "http://192.168.0.161/admin/signin-oidc";
return Task.CompletedTask;
};
But this will result in a
Correlation failed. Unknown location.
exception on redirect.
Edit 2
The
Correlation failed. Unknown location.
exception disappeared without me knowingly doing anything different. Which means I will go with this solution for the moment.
I ran into this scenario when trying to login using an external provider on an Android device and running the web service locally on my PC. I had to use a proxy address since Android doesn't know to forward the localhost calls to the PC.
I am using AddMicrosoftIdentityWebApp instead of AddOpenIdConnect but they are very similar.
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAd", options);
options.SaveTokens = true;
options.Events.OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.RedirectUri = "https://192.168.1.138:45455/signin-oidc";
return Task.CompletedTask;
};
})
.EnableTokenAcquisitionToCallDownstreamApi(
options=> {
options.RedirectUri= "https://192.168.1.138:45455/signin-oidc";
},
initialScopes: GraphConstants.Scopes)
I was also getting the Correlation failed. Unknown location. error when trying to login on the PC. I noticed if the login page was loaded using the local host URL and the redirect was set to the 192.168 address this error would occur. Launching the login page using the 192.168 address stopped that error from happening.
UPDATE: It looks like following code is failing:
services.AddDataProtection()
.SetApplicationName(appname)
.PersistKeysToRedis(redis, "DataProtectionKeys")
.ProtectKeysWithCertificate(LoadCert(Configuration));
It's unable to read certificate from the pfx file.
UPDATE2:
Oh my! Certificate file has been excluded by .gitignore!:)) Live&Learn. At least we lived, right!?;))
Initial question:
I have ASP.NET Core 2.1 app deployed behind AWS load balancer in Docker container.
When I'm trying to log in into app from the login page. I am getting InvalidOperationException with this justification:
No authenticationScheme was specified, and there was no
DefaultChallengeScheme found.
But when I hit same URL again, it actually moving to the proper page and works for a while, then again throws same exception with HTTP status 500
and after 2nd attempt to open same page it succeeds. Interestingly enough Chrome is not as robust as IE: if IE unable to recover after exception Chrome always return 404 on subsequent submission of the page, which produced aforementioned exception.
So I would appreciate, if somebody would be able to provide me with
ideas how to remedy the situation Obviously issue is related to the
authentication, but I could not figure out exactly what should be done.
Here is relevant exert from ConfigureServices() in Startup.cs:
string appname = "MyApp";
var redis = ConnectionMultiplexer.Connect(Configuration.GetConnectionString("RedisConnection"));
services.AddDataProtection()
.SetApplicationName(appname)
.PersistKeysToRedis(redis, "DataProtectionKeys")
.ProtectKeysWithCertificate(LoadCert(Configuration));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication( CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/Area/Ctrl/Login");
options.LogoutPath = new PathString("/Area/Ctrl/Logout");
options.Cookie.IsEssential = true;
});
services.AddDistributedRedisCache(o =>
{
o.Configuration = Configuration.GetConnectionString("RedisConnection");
});
services.AddSession(options =>
{
options.Cookie.Name = appname;
options.IdleTimeout = TimeSpan.FromSeconds(600);
});
Here is relevant code from Configure() in Startup.cs:
app.UseForwardedHeaders();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Ctrl}/{action=Login}/{id?}"
);
});
Here is how I am setting principal in controller, which handles login:
ClaimsIdentity identity = new ClaimsIdentity(GetUserRoleClaims(dbUserData), CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
if (principal == null)
throw new ApplicationException($"Could not create principal for {user?.UserName} user.");
await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
if (httpContext.User == null)
{
httpContext.User = principal;
}
Ok, everything is working now.:)
This is what made a difference:
If app is under load balancing all instances has to share Data Protection encryption keys(e.g. use same key ring). Hence comes the Redis and a cert. Session should also be shared. Hence comes the Redis again.
Certificate for ProtectKeysWithCertificate() call should load correctly. If it could not be loaded do not make that call at all, but that's would be really bad idea. Just figure out why it's not loading.
To avoid InvalidOperationException being thrown in custom authentication HttpContext. User should be assigned manually inside Login action.
One important thing about certificate: Data Protection module supports only certs with CAPI private keys. CNG ones are left behind.