7

I've got an ASP.NET Core MVC app, hosted on Azure websites, where I've implemented Session and Identity. My problem is, after 30 minutes, I get logged out. It doesn't matter if I've been active in the last 30 minutes or not.

Doing some searching, I found that the issue is the SecurityStamp stuff, found here. I've tried implementing this by doing the following:

Here's my UserManager impelmentation with the security stamp stuff:

public class UserManager : UserManager<Login>
{
    public UserManager(
        IUserStore<Login> store,
        IOptions<IdentityOptions> optionsAccessor,
        IPasswordHasher<Login> passwordHasher,
        IEnumerable<IUserValidator<Login>> userValidators,
        IEnumerable<IPasswordValidator<Login>> passwordValidators,
        ILookupNormalizer keyNormalizer,
        IdentityErrorDescriber errors,
        IServiceProvider services,
        ILogger<UserManager<Login>> logger)
        : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
    {
        // noop
    }

    public override bool SupportsUserSecurityStamp => true;

    public override async Task<string> GetSecurityStampAsync(Login login)
    {
        return await Task.FromResult("MyToken");
    }

    public override async Task<IdentityResult> UpdateSecurityStampAsync(Login login)
    {
        return await Task.FromResult(IdentityResult.Success);
    }
}

Here's my ConfigureServices method on Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddApplicationInsightsTelemetry(Configuration);

    services.AddSingleton(_ => Configuration);

    services.AddSingleton<IUserStore<Login>, UserStore>();
    services.AddSingleton<IRoleStore<Role>, RoleStore>();

    services.AddIdentity<Login, Role>(o =>
    {
        o.Password.RequireDigit = false;
        o.Password.RequireLowercase = false;
        o.Password.RequireUppercase = false;
        o.Password.RequiredLength = 6;
        o.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(365);
        o.Cookies.ApplicationCookie.SlidingExpiration = true;
        o.Cookies.ApplicationCookie.AutomaticAuthenticate = true;
    })
        .AddUserStore<UserStore>()
        .AddUserManager<UserManager>()
        .AddRoleStore<RoleStore>()
        .AddRoleManager<RoleManager>()
        .AddDefaultTokenProviders();

    services.AddScoped<SignInManager<Login>, SignInManager<Login>>();
    services.AddScoped<UserManager<Login>, UserManager<Login>>();

    services.Configure<AuthorizationOptions>(options =>
    {
        options.AddPolicy("Admin", policy => policy.Requirements.Add(new AdminRoleRequirement(new RoleRepo(Configuration))));
        options.AddPolicy("SuperUser", policy => policy.Requirements.Add(new SuperUserRoleRequirement(new RoleRepo(Configuration))));
        options.AddPolicy("DataIntegrity", policy => policy.Requirements.Add(new DataIntegrityRoleRequirement(new RoleRepo(Configuration))));
    });

    services.Configure<FormOptions>(x => x.ValueCountLimit = 4096);
    services.AddScoped<IPasswordHasher<Login>, PasswordHasher>();

    services.AddDistributedMemoryCache();
    services.AddSession();

    services.AddMvc();

    // repos
    InjectRepos(services);

    // services
    InjectServices(services);
}

And lastly, here's my Configure method on Startup.cs:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseApplicationInsightsRequestTelemetry();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/home/error");
    }

    app.UseStatusCodePages();

    app.UseStaticFiles();

    app.UseSession();
    app.UseIdentity();

    app.UseMiddleware(typeof (ErrorHandlingMiddleware));
    app.UseMiddleware(typeof (RequestLogMiddleware));

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

What's wrong with my implementation here?

UPDATE: What a second...I noticed my UserManager is not inheriting from any interfaces for the security stamp stuff, is that what's needed?

Community
  • 1
  • 1
ganders
  • 7,285
  • 17
  • 66
  • 114

2 Answers2

6

This is simply because you need to enable and configure Data Protection. The cookie and session setup looks correct. What is happening right now for you is that whenever the app is recycled or the server load balances to another server or a new deployment happens, etc, it creates a new Data protection key in memory, so your users' session keys are invalid. So all you need to do is add the following to Startup.cs:

 services.AddDataProtection()
       .PersistKeysToFileSystem(new DirectoryInfo(@"D:\writable\temp\directory\"))
       .SetDefaultKeyLifetime(TimeSpan.FromDays(14));

Use the documentation to learn how to properly set this up and the different options of where to save the Data Protection key (file system, redis, registry, etc). You could think of the data protection key as the replacement of the web.config's machine key in asp.net.

Since you mentioned you're using Azure, you could use this package Microsoft.AspNetCore.DataProtection.AzureStorage to save the key so that it persists. So you could use this example of how to use Azure Storage.

truemedia
  • 1,042
  • 6
  • 12
  • So my application pool is getting recycled every 30 minutes, and is synced with the time that I've last logged in? That doesn't makes sense to me. – ganders Feb 01 '17 at 15:59
  • It's probably not being recycled that often, no. But the AspNetCore documentation mentions that the Data protection keys are generated in memory by default-- and are what protect the session and identity cookie data (as well as the ValidateAntiForgeryToken action filter) which will not be valid for the user if this data protection key changes. – truemedia Feb 01 '17 at 16:08
  • I just noticed [the default settings for Data Protection](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/default-settings#data-protection-default-settings) on Azure are actually quite good at setting you up. You can simply add the code, `services.AddDataProtection();` and you should be up and running. Azure will automatically persist the keys for you. Also, for anyone using IIS, they will also work well on a single machine without needing the `PersistKeysToFileSystem` option, since the default settings set this up well, as shown in the docs. – truemedia Feb 02 '17 at 14:57
  • I tried adding that one line of code to my ConfigureServices method directly after this line: `services.AddSingleton(_ => Configuration);` and it still did not work. – ganders Feb 02 '17 at 19:33
1

Are you hosted under IIS? If so, maybe nothing is wrong with your code, but your application pool could get recycled (check advanced settings on the application pool). When that happen, does your binary get unloaded from memory and replaced by a new one, its PID changing ?

Daboul
  • 2,635
  • 1
  • 16
  • 29