2

I am using ASP Identity for authentication. Part of the Statup.Auth.cs looks like:

            app.UseCookieAuthentication(
            new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, 
                LoginPath = new PathString("/Account/Login"), 
                Provider = new CookieAuthenticationProvider
                           {
                               // Enables the application to validate the security stamp when the user logs in.
                               // This is a security feature which is used when you change a password or add an external login to your account.  
                               OnValidateIdentity =
                                   SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                                       validateInterval: TimeSpan.FromMinutes(30),
                                       regenerateIdentity:
                                   (manager, user) =>
                                   user.GenerateUserIdentityAsync(manager))
                           },
                ExpireTimeSpan = TimeSpan.FromMinutes(Settings.Default.SessionExpireTimeoutInMinutes), 
            });

and part of my Login method:

SignInStatus result = await this.SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false);

If a user selects "remember me" option, he should not be logged out until something like 30 days. If he doesn't select this option, he should be logged out automatically after some short period of time, let's say 10 minutes. At the moment user is logged out no matter if he selected the option.

rideronthestorm
  • 727
  • 1
  • 13
  • 32

2 Answers2

3

There is a known bug in the SecurityStampValidator class that prevents the isPersistent property from being preserved when it resets the authentication cookie.

The issue is supposedly resolved so you should try updating all your packages, but some people are still having issues.

A way to fix the issue is to write your own SecurityStampValidator class. You can find a version of the Microsoft source code here.

Here's some code I tried that seems to work (I added the AuthenticationProperties with AllowRefresh and IsPersistent properties set):

using Microsoft.Owin.Security.Cookies;
using Owin;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin.Security;

namespace Microsoft.AspNet.Identity.Owin
{
    /// <summary>
    /// Static helper class used to configure a CookieAuthenticationProvider to validate a cookie against a user's security stamp
    /// </summary>
    public static class MySecurityStampValidator
    {
        /// <summary>
        /// Can be used as the ValidateIdentity method for a CookieAuthenticationProvider which will check a user's security stamp after validateInterval
        /// Rejects the identity if the stamp changes, and otherwise will call regenerateIdentity to sign in a new ClaimsIdentity
        /// </summary>
        /// <typeparam name="TManager"></typeparam>
        /// <typeparam name="TUser"></typeparam>
        /// <param name="validateInterval"></param>
        /// <param name="regenerateIdentity"></param>
        /// <returns></returns>
        public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser>(TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentity)
            where TManager : UserManager<TUser, string>
            where TUser : class, IUser<string>
        {
            return OnValidateIdentity<TManager, TUser, string>(validateInterval, regenerateIdentity, (id) => id.GetUserId());
        }

        /// <summary>
        /// Can be used as the ValidateIdentity method for a CookieAuthenticationProvider which will check a user's security stamp after validateInterval
        /// Rejects the identity if the stamp changes, and otherwise will call regenerateIdentity to sign in a new ClaimsIdentity
        /// </summary>
        /// <typeparam name="TManager"></typeparam>
        /// <typeparam name="TUser"></typeparam>
        /// <typeparam name="TKey"></typeparam>
        /// <param name="validateInterval"></param>
        /// <param name="regenerateIdentityCallback"></param>
        /// <param name="getUserIdCallback"></param>
        /// <returns></returns>
        public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser, TKey>(TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentityCallback, Func<ClaimsIdentity, TKey> getUserIdCallback)
            where TManager : UserManager<TUser, TKey>
            where TUser :class, IUser<TKey>
            where TKey : IEquatable<TKey>
        {
            return async (context) =>
            {
                DateTimeOffset currentUtc = DateTimeOffset.UtcNow;
                if (context.Options != null && context.Options.SystemClock != null)
                {
                    currentUtc = context.Options.SystemClock.UtcNow;
                }
                DateTimeOffset? issuedUtc = context.Properties.IssuedUtc;

                // Only validate if enough time has elapsed
                bool validate = (issuedUtc == null);
                if (issuedUtc != null)
                {
                    TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value);
                    validate = timeElapsed > validateInterval;
                }
                if (validate)
                {
                    var manager = context.OwinContext.GetUserManager<TManager>();
                    var userId = getUserIdCallback(context.Identity);
                    if (manager != null && userId != null)
                    {
                        var user = await manager.FindByIdAsync(userId).ConfigureAwait(false);
                        bool reject = true;
                        // Refresh the identity if the stamp matches, otherwise reject
                        if (user != null && manager.SupportsUserSecurityStamp)
                        {
                            string securityStamp = context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType);
                            if (securityStamp == await manager.GetSecurityStampAsync(userId).ConfigureAwait(false))
                            {
                                reject = false;
                                // Regenerate fresh claims if possible and resign in
                                if (regenerateIdentityCallback != null)
                                {
                                    ClaimsIdentity identity = await regenerateIdentityCallback.Invoke(manager, user);
                                    if (identity != null)
                                    {
                                        var isPersistent = context.Properties.IsPersistent;
                                        AuthenticationProperties prop = new AuthenticationProperties();
                                        prop.AllowRefresh = true; //without this, will log out after 30 minutes
                                        prop.IsPersistent = isPersistent; //without this, will log out after 30 minutes, or whenever the browser session is ended
                                        context.OwinContext.Authentication.SignIn(prop, identity);
                                    }
                                }
                            }
                        }
                        if (reject)
                        {
                            context.RejectIdentity();
                            context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
                        }
                    }
                }
            };
        }
    }
}

You then use this by changing SecurityStampValidator to MySecurityStampValidator in the Startup.Auth.cs file, assuming you are using the standard MVC project template.

David Sopko
  • 5,263
  • 2
  • 38
  • 42
Matthew
  • 4,149
  • 2
  • 26
  • 53
0

In my case, the problem was in machineKey. The server that's hosting my website keeps changing its machineKey that is used in encrypting and decryption the tickets and the solution was to assign a fixed machineKey in Web.Config.

Check out this solution here.

Ashi
  • 806
  • 1
  • 11
  • 22