Okay this is the full solution I have come up with which partly uses what @jacktric suggested but also allows for validating the security stamp if a users password has been changed elsewhere. Please let me know if anyone can recommend any improvements or see any downfalls in my solution.
I have removed the OnValidateIdentity section from the UseCookieAuthentication section as follows:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnApplyRedirect = ctx =>
{
if (!IsAjaxRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
}
});
I then have the following IActionFilter that is registered in the FilterConfig.cs which checks if the user is logged in (I have parts of the system that can be accessed by anonymous users) and whether the current security stamp matches the one from the database. This check is made every 30 minutes using sessions to find out when the last check was.
public class CheckAuthenticationFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext filterContext)
{
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
try
{
// If not a child action, not an ajax request, not a RedirectResult and not a PartialViewResult
if (!filterContext.IsChildAction
&& !filterContext.HttpContext.Request.IsAjaxRequest()
&& !(filterContext.Result is RedirectResult)
&& !(filterContext.Result is PartialViewResult))
{
// Get current ID
string currentUserId = filterContext.HttpContext.User.Identity.GetUserId();
// If current user ID exists (i.e. it is not an anonymous function)
if (!String.IsNullOrEmpty(currentUserId))
{
// Variables
var lastValidateIdentityCheck = DateTime.MinValue;
var validateInterval = TimeSpan.FromMinutes(30);
var securityStampValid = true;
// Get instance of userManager
filterContext.HttpContext.GetOwinContext().Get<DbContext>().Database.Connection.ConnectionString = DbContext.GetConnectionString();
var userManager = filterContext.HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
// Find current user by ID
var currentUser = userManager.FindById(currentUserId);
// If "LastValidateIdentityCheck" session exists
if (HttpContext.Current.Session["LastValidateIdentityCheck"] != null)
DateTime.TryParse(HttpContext.Current.Session["LastValidateIdentityCheck"].ToString(), out lastValidateIdentityCheck);
// If first validation or validateInterval has passed
if (lastValidateIdentityCheck == DateTime.MinValue || DateTime.Now > lastValidateIdentityCheck.Add(validateInterval))
{
// Get current security stamp from logged in user
var currentSecurityStamp = filterContext.HttpContext.User.GetClaimValue("AspNet.Identity.SecurityStamp");
// Set whether security stamp valid
securityStampValid = currentUser != null && currentUser.SecurityStamp == currentSecurityStamp;
// Set LastValidateIdentityCheck session variable
HttpContext.Current.Session["LastValidateIdentityCheck"] = DateTime.Now;
}
// If current user doesn't exist or security stamp invalid then log them off
if (currentUser == null || !securityStampValid)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary { { "Controller", "Account" }, { "Action", "LogOff" }, { "Area", "" } });
}
}
}
}
catch (Exception ex)
{
// Log error
}
}
}
I have the following extension methods for getting and updating claims for the logged in user (taken from this post https://stackoverflow.com/a/32112002/1806809):
public static void AddUpdateClaim(this IPrincipal currentPrincipal, string key, string value)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return;
// Check for existing claim and remove it
var existingClaim = identity.FindFirst(key);
if (existingClaim != null)
identity.RemoveClaim(existingClaim);
// Add new claim
identity.AddClaim(new Claim(key, value));
// Set connection string - this overrides the default connection string set
// on "app.CreatePerOwinContext(DbContext.Create)" in "Startup.Auth.cs"
HttpContext.Current.GetOwinContext().Get<DbContext>().Database.Connection.ConnectionString = DbContext.GetConnectionString();
var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identity), new AuthenticationProperties() { IsPersistent = true });
}
public static string GetClaimValue(this IPrincipal currentPrincipal, string key)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return null;
var claim = identity.Claims.FirstOrDefault(c => c.Type == key);
return claim.Value;
}
And finally anywhere that the users password is updated I call the following, this updates the security stamp for the user whose password is being edited and if it is the current logged in users password that is being edited then it updates the securityStamp claim for the current user so that they will not get logged out of their current session the next time the validity check is made:
// Update security stamp
UserManager.UpdateSecurityStamp(user.Id);
// If updating own password
if (GetCurrentUserId() == user.Id)
{
// Find current user by ID
var currentUser = UserManager.FindById(user.Id);
// Update logged in user security stamp (this is so their security stamp matches and they are not signed out the next time validity check is made in CheckAuthenticationFilter.cs)
User.AddUpdateClaim("AspNet.Identity.SecurityStamp", currentUser.SecurityStamp);
}