Categories
Uncategorized

ASP.NET Identity – invalidate all sessions on SecurityStamp update

I develop the account system for my upcoming game Battlefall with ASP.NET Identity and ASP.NET MVC 5.
I came across the problem, that ASP.NET Identity will not invalidate sessions if the SecurityStamp has changed. This behavior is very important. Let’s say you are logged in from two different browsers with the same user. If you change the password in one browser, the same user in the other browser won’t be logged out!

To add the described behavior, you must modify the following code from a new generated ASP.NET MVC 5 project.
I basically added the SecurityStamp as a Claim to the user session. Every time a user requests a page, I will compare the SecurityStamp from the Claim with the current value from the Database.

AccountController
[code language=”csharp” highlight=”1-11,17,18, 38,39″]
private async Task SignInAsync(ApplicationUser user)
{
Claim rememberMeClaim = AuthenticationManager.User.FindFirst("RememberMe");
if (rememberMeClaim != null)
{
bool rememberMe;
bool.TryParse(rememberMeClaim.Value, out rememberMe);
await SignInAsync(user, rememberMe);
}
await SignInAsync(user, false);
}

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
identity.AddClaim(new Claim("SecurityStamp", user.SecurityStamp));
identity.AddClaim(new Claim("RememberMe", isPersistent.ToString()));
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

//
// POST: /Account/Manage
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Manage(ManageViewModel model)
{
bool hasPassword = HasPassword();
ViewBag.HasLocalPassword = hasPassword;
ViewBag.ReturnUrl = Url.Action("Manage");
if (hasPassword)
{
if (ModelState.IsValid)
{
IdentityResult result = await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword, model.NewPassword);
if (result.Succeeded)
{
// update SecurityStamp to prevent logout of this user
await SignInAsync(IdentityManager.User);

return RedirectToAction("Manage", new { Message = ManageMessageId.ManageSuccess });
}
else
{
AddErrors(result);
}
}
}
else
{
// User does not have a password so remove any validation errors caused by a missing OldPassword field
ModelState state = ModelState["OldPassword"];
if (state != null)
{
state.Errors.Clear();
}

if (ModelState.IsValid)
{
IdentityResult result = await UserManager.AddPasswordAsync(User.Identity.GetUserId(), model.NewPassword);
if (result.Succeeded)
{
return RedirectToAction("Manage", new { Message = ManageMessageId.SetPasswordSuccess });
}
else
{
AddErrors(result);
}
}
}

// If we got this far, something failed, redisplay form
return View(model);
}
[/code]

Startup
[code language=”csharp” highlight=”8-29,”]
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = ctx =>
{
var ret = Task.Run(() =>
{
Claim claim = ctx.Identity.FindFirst("SecurityStamp");
if (claim != null)
{
UserManager<ApplicationUser> userManager = new UserManager<Models.ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));
var user = userManager.FindById(ctx.Identity.GetUserId());

// invalidate session, if SecurityStamp has changed
if (user != null && user.SecurityStamp != null && user.SecurityStamp != claim.Value)
{
ctx.RejectIdentity();
}
}
});
return ret;
}
}
});
// Use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}
[/code]

Fortunately, the Katana Project is open source. It helped a lot to solve this issue.