Home > Back-end >  ASP.NET Core 6: Users are logged out after 30 minutes, need 6 hours
ASP.NET Core 6: Users are logged out after 30 minutes, need 6 hours

Time:01-17

I have an issue where no matter which settings I adjust, a user who has signed in is forcibly logged out after 30 minutes. I would like to extend this timeframe to 6 hours.

I am using a system where login credentials are provided externally. The system receives a JWT and uses the information contained there to authenticate the user. This is the root cause of the issue; users that log in via the .NET Framework's built-in authentication system don't have this problem. I don't know why or how.

Here are the highlights of what I have tried:

configuring application cookies in Program.cs:

builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.Name = ".AspNetCore.Identity.Application";
    options.ExpireTimeSpan = TimeSpan.FromHours(6);
    options.Cookie.MaxAge = TimeSpan.FromHours(6);
    options.SlidingExpiration = true;
});

Using persistence and authentication properties in the authentication process:

await _signInManager.SignInAsync( 
   user, 
   new AuthenticationProperties {
      IsPersistent = true,
      ExpiresUtc = DateTime.UtcNow.AddHours(6)
   });

I have ensured I have data protection enabled in Program.cs:

builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
    .SetDefaultKeyLifetime(TimeSpan.FromDays(14));

I have published it to a dedicated IIS server where I set the Idle Timeout to 6 hours.

I have set SlidingExpiration = true in all the places that have the option to do so.

I have tried all of the above together, separate, and in combination. User sessions are still restricted to 30 minutes. When I set the Application Cookies to expire after 1 second, the session expires after 1 second. But when I set them to 6 hours, it still expires at 30 minutes. I have no idea why.

What am I missing here? I have been struggling with this for days and have still found no solution. I've found similar questions on Stack Overflow and their solutions all include what I have tried here, to no avail.

I have double-checked and noticed that the 30 minute time-out does not affect users that authenticate via the .NET Framework's built-in login system (where they enter their username/password). This only seems to affect users that authenticate using the credentials delivered via JWT.

Here is the full Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("ConnectionString");
builder.Services.AddDbContext<DBContext>(options => options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddControllersWithViews();

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromHours(6);
});

//Add JWT bearer and denied paths
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = ".AspNetCore.Identity.Application";
        options.Cookie.MaxAge = TimeSpan.FromHours(6);
        options.SlidingExpiration = true;
        options.LoginPath = "/Account/Unauthorized/";
        options.AccessDeniedPath = "/Account/Forbidden/";
    })
            .AddJwtBearer(x =>
            {
                x.RequireHttpsMetadata = false;
                x.SaveToken = true;
                x.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = false,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = builder.Configuration["Jwt:Issuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("Jwt:Key"))
                };
            });

//GDPR compliance
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;
});

builder.Services.ConfigureNonBreakingSameSiteCookies();

builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.Name = ".AspNetCore.Identity.Application";
    options.Cookie.MaxAge = TimeSpan.FromHours(6);
    options.SlidingExpiration = true;
    options.Events = new CookieAuthenticationEvents
    {
        OnRedirectToLogin = x =>
        {
            x.Response.Redirect("https://localhost:44329/Expired/Index/");
            return Task.CompletedTask;
        }
    };
    options.ExpireTimeSpan = TimeSpan.FromDays(14);
});

//define policy for different authorization
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Administrator"));
    options.AddPolicy("UsersOnly", policy => policy.RequireRole("User", "Editor", "Author"));
    options.AddPolicy("RequireApprovedUser", policy => policy.Requirements.Add(new ApprovedUserRequirement(true)));
});

builder.Services.AddScoped<IAuthorizationHandler, ApprovedUserRequirementHandler>();

//Data Protection configuration
var keysFolder = Path.Combine(builder.Environment.ContentRootPath, "UserKeys");
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
    .SetDefaultKeyLifetime(TimeSpan.FromDays(14));


builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true).AddEntityFrameworkStores<DBContext>();

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromHours(6);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.SignIn.RequireConfirmedAccount = true;
})
    .AddDefaultTokenProviders()
    .AddDefaultUI()
    .AddEntityFrameworkStores<DBContext>();

builder.Services.AddRazorPages();

//this will get the email settings in appsettings
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));

//Use for sending email
builder.Services.AddTransient<IMailService, Project.Services.EmailSender>();

// Register the Google Analytics configuration
builder.Services.Configure<GoogleAnalyticsOptions>(options => builder.Configuration.GetSection("GoogleAnalytics").Bind(options));

// Register the TagHelperComponent for Google Analytics
builder.Services.AddTransient<ITagHelperComponent, GoogleAnalyticsTagHelper>();

builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, UserClaimsPrincipalFactory<IdentityUser, IdentityRole>>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}
app.Use(async (context, next) =>
{
    await next();
    if (context.Response.StatusCode >= 400)
    {
        context.Request.Path = "/Error/Index/"   context.Response.StatusCode;
        await next();
    }
});

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseCors(builder => builder.AllowAnyOrigin()
                                .WithOrigins("https://localhost:44329", "https://localhost:44329")
                                .AllowAnyMethod()
                                .AllowAnyHeader());

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

app.UseSession();

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

app.Run();

Here is the code that authenticates incoming users:

[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> TokenAuthenticationSignInAsync([FromQuery] string id)
{
    try
    {
        string qstring = HttpContext.Request.Query["jwt"];

        //This parses the JWT into a UserPOCO
        ParseJWT parseJWT = new ParseJWT(_config);
        //UserPOCO has 3 properties: Name (string), Email (string), and ValidToken (bool)
        UserPOCO user = parseJWT.ParseToUser(qstring);

        if (user.ValidToken != true)
        {
            _logger.LogWarning($"Invalid Login");
            return RedirectToAction("Index", "Forbidden");
        }
        else
        {
            IdentityUser iUser = new()
            {
                UserName = user.Name,
                Email = user.Email,
                EmailConfirmed = true
            };
            await _signInManager.SignInAsync( 
                iUser, 
                new AuthenticationProperties {
                    IsPersistent = true,
                    ExpiresUtc = DateTime.UtcNow.AddHours(6),
                    AllowRefresh = true
                });

            return RedirectToAction("Index", "Dashboard");
        }
        
    }
    catch (System.Exception e)
    {
        _logger.LogError(e, $"Error in {nameof(TokenAuthenticationSignIn)}");
        return NotFound();
    }

}

Here are the settings I used in IIS. Note that I will have trouble adjusting settings unavailable on Plesk.

Plesk IIS Settings. Idle Timeout is set to 6 hours.

CodePudding user response:

Welp, it took a lot of digging, but I found the solution.

I needed to ensure that users were saved to the IdentityUser tables using _userManager.CreateAsync(). The code for registering users I used would create a new IdentityUser every time without ensuring it went in the database. Without an entry in the IdentityUser tables, .NET Core can't use keys or sessions properly.

Here is the change to that code:

[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> TokenAuthenticationSignInAsync([FromQuery] string id)
{
    try
    {
        string qstring = HttpContext.Request.Query["jwt"];

        //This parses the JWT into a UserPOCO
        ParseJWT parseJWT = new ParseJWT(_config);
        //UserPOCO has 3 properties: Name (string), Email (string), and ValidToken (bool)
        UserPOCO user = parseJWT.ParseToUser(qstring);

        if (user.ValidToken != true)
        {
            _logger.LogWarning($"Invalid Login");
            return RedirectToAction("Index", "Forbidden");
        }
        else
        {
            IdentityUser? iUser;
            //check to see if user is in IdentityUser
            iUser = await _userManager.FindByEmailAsync(user.Email);

            //if user is not in IdentityUser, create a new one:
            if(iUser == null)
            {
               iUser = new()
               {
                   UserName = user.Name,
                   Email = user.Email,
                   EmailConfirmed = true
               };
  
               //Very important! This allows the system to remember users
               //Performing this operation also gives iUser the necessary keys
               await _userManager.CreateAsync(iUser);
            }
            
            await _signInManager.SignInAsync( 
                iUser, 
                new AuthenticationProperties {
                    IsPersistent = true,
                    ExpiresUtc = DateTime.UtcNow.AddHours(6),
                    AllowRefresh = true
                });

            return RedirectToAction("Index", "Dashboard");
        }
        
    }
    catch (System.Exception e)
    {
        _logger.LogError(e, $"Error in {nameof(TokenAuthenticationSignIn)}");
        return NotFound();
    }

}
  • Related