Search code examples
asp.net-coreasp.net-identity-3

ASP.NET Core Web App forced to login frequently


I've got an ASP.NET Core 3.1 Web App that uses ASP.NET Identity for authentication. Everything works fine when I run it locally, but when I deploy to a hosting provider I find that users are asked to log in very frequently (after only a few minutes of inactivity). My guess at this point is that the timing is related to the application pool recycling, because I can force the login by recycling the app pool manually. Unfortunately I have no control over the app pool settings on the server - the idle timeout is set at 5 minutes.

If I understand ASP.NET Identity (and that might be a stretch), the authentication token is stored in a cookie and transmitted with every request. I can't see how the app pool recycling would have any impact on validating that cookie. I could see that happening if it was stored in session state on the server, but I don't think that's happening.

My ConfigureServices method is pretty stock, other than that I've added a few fields to the standard User and Role objects.

public void ConfigureServices(IServiceCollection services) {
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseMySql(Configuration.GetConnectionString("DefaultConnection"))
    );

    services.AddIdentity<User, Role>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.Configure<IdentityOptions>(options => {
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = true;
        options.Password.RequiredLength = 8;

        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
        options.Lockout.MaxFailedAccessAttempts = 5;
        options.Lockout.AllowedForNewUsers = true;

        options.SignIn.RequireConfirmedEmail = true;
        options.SignIn.RequireConfirmedPhoneNumber = false;

        options.User.RequireUniqueEmail = false;
    });

    services.ConfigureApplicationCookie(options => {
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.AccessDeniedPath = "/Identity/Account/AccessDenied";
        options.LoginPath = "/Identity/Account/Login";
        options.ExpireTimeSpan = TimeSpan.FromDays(30);
        options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
        options.SlidingExpiration = true;
    });

    services.AddScoped<IUserClaimsPrincipalFactory<User>, ApplicationUserClaimsPrincipalFactory>();

    services.AddSingleton<IEmailService, EmailService>();
    services.AddTransient<DatabaseInitializationHelper>();

    services.AddControllersWithViews();
    services.AddRazorPages();

    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddOptions<CaptchaSettings>().Bind(Configuration.GetSection("Captcha"));
    services.AddTransient<CaptchaVerificationService>();
}

The Configure method is pretty standard as well.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
    if (env.IsDevelopment() || env.EnvironmentName == "Dev") {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    //app.UseSession();

    app.UseEndpoints(endpoints => {

        endpoints.MapControllerRoute(
            name: "Default",
            pattern: "{controller=Home}/{action=Index}/{id?}");

        endpoints.MapControllerRoute(
            name: "MembersArea",
            pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

        endpoints.MapControllerRoute(
            name: "api",
            pattern: "_api/{controller}/{id?}");
        endpoints.MapRazorPages();
    });
}

Could this be related to the app pool recycling? If so, any way to get around the issue without having any access to the app pool settings? If not, what else could be causing it?


Solution

  • Turns out this was related to Data Protection and I was able to resolve it by using this answer. Apparently my hosting provider limits permissions that ended up preventing access to locations where this type of information would typically be stored.