Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to change the cookie ExpireTimeSpan in Asp.Net Identity after ConfigureAuth

We have a product that is using Asp.Net identity where we want the cookie expiry time to be configurable. The ExpireTimeSpan is currently set in the Startup.ConfigureAuth class that the Visual Studio creates for you with a new project. This is getting the time from a configuration file on startup but we want to be able to change this value from a webAPI. As it stands, the webAPI request can modify the config file but we need to recycle the app pool to get it to take effect.

Is there any way to change this value once the server is already up and running?

All I found on this subject is this question ASP.NET Identity Change ExpireTimeSpan after ConfigureAuth but it is asking for a way to change it for a single session whereas I want to change it globally for the whole server.

Update: I have found from looking through the Katana source that the options appear to be stored in a public property of the class Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware but I don't know of any way to get a reference to the object being used from within my app.

like image 839
Mog0 Avatar asked Nov 10 '22 13:11

Mog0


1 Answers

This took some time to figure out, but I believe I have found the correct solution. It's funny because I already had the solution, I just didn't explore the options available in the scope.

CookieAuthenticationOptions provides only one hook, a delegate property OnValidateIdentity.

The OnValidateIdentity occurs each time someone logs in(via cookie auth provider), which happens to be the perfect time to run some custom logic that determines their new Expiration time. It also lets you configure it globally.

I've debugged it and can confirm the value for the configuration option sticks, and remains for future logins globally until app pool recycle. And also that the value override for expiration applies to the authenticated user after signin callback.

So it's up to you what logic determines the value, and whether you want to set that on a per-person level, or global level.

Here is the code sample to help paint a better picture of this..

public void ConfigureAuth(IAppBuilder app) {
    app.CreatePerOwinContext(ApplicationDbContext.Create);
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
    app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
    app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
    app.CreatePerOwinContext<ApplicationGroupManager>(ApplicationGroupManager.Create);
    app.CreatePerOwinContext<ApplicationDepartmentManager>(ApplicationDepartmentManager.Create);

    app.UseCookieAuthentication(new CookieAuthenticationOptions {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Login"),
        Provider = new CookieAuthenticationProvider {

            OnValidateIdentity = delegate(CookieValidateIdentityContext context) {
                var stampValidator = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser, int>(
                    validateInterval: TimeSpan.FromMinutes(15),
                    regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
                    getUserIdCallback: (id) => (id.GetUserId<int>())
                );
                Task result = stampValidator.Invoke(context);

                bool found_custom_expiration = false;
                int timeout = STATIC_CONFIG.DefaultSessionExpireMinutes;
                DBHelper.Query(delegate (SqlCommand cmd) {
                    cmd.CommandText = @"
                        SELECT [value] 
                        FROM [dbo].[Vars] 
                        WHERE [FK_Department] = (
                            SELECT [Id] FROM [dbo].[ApplicationDepartments] WHERE [title] = 'Default'
                        ) 
                        AND [name]='Session Timeout'
                    ";
                    return cmd;
                }, delegate (SqlDataReader reader) {
                    timeout = reader["value"].ToInt32();
                    found_custom_expiration = true;
                    return false;
                });
                if (found_custom_expiration) {
                    // set it at GLOBAL level for all users.
                    context.Options.ExpireTimeSpan = TimeSpan.FromMinutes(timeout);
                    // set it for the current user only.
                    context.Properties.ExpiresUtc = context.Properties.IssuedUtc.Value.AddMinutes(timeout);
                }
                // store it in a claim, so we can grab the remaining time later.
                // credit: https://stackoverflow.com/questions/23090706/how-to-know-when-owin-cookie-will-expire
                var expireUtc = context.Properties.ExpiresUtc;
                var claimType = "ExpireUtc";
                var identity = context.Identity;
                if (identity.HasClaim(c => c.Type == claimType)) {
                    var existingClaim = identity.FindFirst(claimType);
                    identity.RemoveClaim(existingClaim);
                }
                var newClaim = new System.Security.Claims.Claim(claimType, expireUtc.Value.UtcTicks.ToString());
                context.Identity.AddClaim(newClaim);
                return result;
            }
        },
        SlidingExpiration = true,
        // here's the default global config which was seemingly unchangeable.. 
        ExpireTimeSpan = TimeSpan.FromMinutes(STATIC_CONFIG.DefaultSessionExpireMinutes)
    });

    ApplicationHandler.OwinStartupCompleted();

}

That could definitely be improved to issue the change only when not already changed. And you don't have to use a database query, it could be an XML read, or web.config appsetting, or whatever. The identity is in place... So you could even access the context.Identity and perform some kind of custom check on a ApplicationUser .

So... the relevant bit...

OnValidateIdentity = delegate(CookieValidateIdentityContext context) {
            var stampValidator = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser, int>(
                validateInterval: TimeSpan.FromMinutes(30),
                regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
                getUserIdCallback: (id) => (id.GetUserId<int>())
            );
            Task result = stampValidator.Invoke(context);

            int my_custom_minutes = 60; // run your logic here.
                // set it at GLOBAL level for all (future) users.
                context.Options.ExpireTimeSpan = TimeSpan.FromMinutes( my_custom_minutes );
                // set it for the current user only.
                context.Properties.ExpiresUtc = context.Properties.IssuedUtc.Value.AddMinutes( my_custom_minutes );

            return result;
}
like image 141
Barry Avatar answered Nov 14 '22 21:11

Barry