Dynamic database connection using Asp.Net identity - asp.net-mvc

I am working on a multi-tenant application that uses multiple databases. There is one master database that contains user information and then each tenant database also has their own users for that tenant (which are a subset of the users in the master database).
The user will log in which will check the master database, then based on their details (i.e. which tenant they belong to) it will log them into the application using the user details on their tenant database.
I am using the method described in this thread (Dynamic database connection using Asp.net MVC and Identity2) to set the database for UserManager each time because at the point that the application starts it will not know what database to use therefore the following code in "Startup.Auth" would be setting the incorrect database:
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
This seems to be working well for most things but one problem I have is with the user getting logged out after the time set in "validateInterval" shown in the code below (this has been set to 20 seconds for testing):
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.FromSeconds(20),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),
OnApplyRedirect = ctx =>
{
if (!IsAjaxRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
}
});
I think the problem might be because when the code above is called in the "Startup.Auth" file it does not know what database to use however I have not confirmed this.
If I debug the "GenerateUserIdentityAsync" code I can see that it is getting the correct "securityStamp" for the user from the client database which makes me think it is finding the correct database but I cannot work out why it is still logging out the user after the time set for "validateInterval".
Can anyone offer any advice on how this can be resolved or at least possible ways to try and debug what the problem might be?

I have experienced the same issue on my multi-tenant ASP.NET MVC app.
If your goal is to set an expiration time for the logged-in user just remove the code in CookieAuthenticationProvider and set the ExpireTimeSpan property in the parent CookieAuthenticationOptions.
Your code should be:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
ExpireTimeSpan = TimeSpan.FromMinutes(15), //cookie expiration after 15 mins of user inactivity
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
}
});
Hope this helps.

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);
}

Related

The connection is not found error in auth0

I am using auth0 to maintain users details. and I am following This article.
I downloaded project which is using default lock screen, and it's working fine, I am able to create user and login, logout.
When I try with custom login view, and when I try to login with my credentials, It's giving me error "The connection is not found".
I am not sure, Which connection I need to pass here.
Below is the code which I paste from mentioned article.
[HttpPost]
public async Task<ActionResult> Login(LoginViewModel vm, string returnUrl = null)
{
if (ModelState.IsValid)
{
try
{
AuthenticationApiClient client =
new AuthenticationApiClient(
new Uri($"https://{ConfigurationManager.AppSettings["auth0:Domain"]}/"));
var result = await client.AuthenticateAsync(new AuthenticationRequest
{
ClientId = ConfigurationManager.AppSettings["auth0:ClientId"],
Scope = "openid",
Connection = "Database-Connection", // Specify the correct name of your DB connection
Username = vm.EmailAddress,
Password = vm.Password
});
// Get user info from token
var user = await client.GetTokenInfoAsync(result.IdToken);
// Create claims principal
var claimsIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, user.UserId),
new Claim(ClaimTypes.Name, user.FullName ?? user.Email),
new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "ASP.NET Identity", "http://www.w3.org/2001/XMLSchema#string")
}, DefaultAuthenticationTypes.ApplicationCookie);
// Sign user into cookie middleware
AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = false }, claimsIdentity);
return RedirectToLocal(returnUrl);
}
catch (Exception e)
{
ModelState.AddModelError("", e.Message);
}
}
return View(vm);
}
Below is Error I am getting,
I am passing correct connection name in AuthenticationRequest also as below image,
You need to make sure that the Connection name specified in the AuthenticationRequest instance that you pass into AuthenticateAsync maps to an existing database connection within your Auth0 account.
More specifically, you're currently passing "Database-Connection"as the name of the connection and you need to ensure that either a connection with that name exists or change your code:
new AuthenticationRequest {
// ...
Connection = "Database-Connection",
Username = vm.EmailAddress,
Password = vm.Password
}

OWIN Cookies not being removed by IE

I'm experiencing a problem where the following code does not appear to be working as expected:
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
When this is executed the above line of code I would expect the following property to be false
AuthenticationManager.User.Identity.IsAuthenticated
But it is not, it remains true (FOR IE ONLY, I'm using IE 11)
I'm exposing my Owin context through the following property:
public IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.Current.GetOwinContext().Authentication;
}
}
I am also attempting to manually remove the authentication cookie using the following code:
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
var appCookie = HttpContext.Current.Request.Cookies[".AspNet.AuthCookie"];
if (appCookie != null)
{
appCookie = new HttpCookie(".AspNet.AuthCookie");
appCookie.Expires = DateTime.Now.AddYears(-1);
HttpContext.Current.Response.Cookies.Add(appCookie);
}
Which, again, does not work in IE. It works in Chrome.
FYI, i am manually setting the name of the cookie in my Startup.Auth.cs
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
ExpireTimeSpan = TimeSpan.FromMinutes(Config.AuthenticationCookieTimeOutMinutes),
CookieHttpOnly = true,
CookieName = ".AspNet.AuthCookie",
LoginPath = new PathString("/Account/Login"),
CookieSecure = CookieSecureOption.SameAsRequest
});
There is plenty online about the Signout() method not working properly, but most solutions are to remove the cookie manually, which i am having no joy with. Why would this not work in IE 11?
UPDATE
So I was unable to fix the issue with cookies in IE, however was able to overcome this issue by overriding the HandleUnauthorizedRequest virtual of the AuthorizeAttribute to see if the user has been authenticated, then double checks the CurrentUser held in session
protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
{
UserAccount currentUser = (UserAccount)filterContext.HttpContext.Session[SessionStrings.CurrentUser];
if (filterContext.HttpContext.Request.IsAuthenticated && currentUser != null)
{
...
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}

How to do programmatic sign in using aspnet Identity Framework v2?

I'm cobbling together snippets of code from blogs and different places to try to get this to work. Normally, I'd refer to the reference documentation, but I can't find it here or anywhere else. It's just videos and demos for specific use cases that include user management or facebook or twitter.
I have a proprietary authentication service that I'm using. User accounts are not managed inside my application. So I need to be able to sign in a user that's completely constructed at run time.
Here's what I'm trying now in my MVC app.
using System.Security.Claims;
public class HomeController : Controller {
public ActionResult Scratch() {
var claims = new Claim[] {
new Claim(ClaimTypes.Name, "somename"),
new Claim(ClaimTypes.NameIdentifier, "someidentifier"),
new Claim("foo", "bar"),
};
var identity = new ClaimsIdentity(claims);
var authenticationManager = HttpContext.GetOwinContext().Authentication;
authenticationManager.SignIn(identity);
return Content(
$"authentication manager type: {authenticationManager.GetType()} \n"
+ $"authenticated: {HttpContext.User.Identity.IsAuthenticated} \n"
+ $"user name: {HttpContext.User.Identity.Name} \n",
"text/plain");
}
}
The output is
authentication manager type: Microsoft.Owin.Security.AuthenticationManager
authenticated: False
user name:
Questions:
Why does the output show that the user has not been authenticated? What more do I have to do to get this user authenticated?
Where is the documentation for this framework?
Update
Startup.cs
public partial class Startup {
public void Configuration(IAppBuilder app) {
ConfigureAuth(app);
ConfigureAnalyticContext(app);
}
}
Startup.Auth.cs:
(there is actually much more, but all the rest has been commented out, in search of finding a minimal configuration that works)
public partial class Startup {
public void ConfigureAuth(IAppBuilder app) {
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}
}
AnalyticContext.Auth.cs
(this is my Entity Framework context, I doubt it's related to this problem)
public partial class Startup {
public void ConfigureAnalyticContext(IAppBuilder app) {
app.CreatePerOwinContext(() => CentoAnalyticsContext.Create());
}
}
Well, it seems that you are not using ASP.NET Identity. ASP.NET Identity is new membership system of asp.net, which automatically creates database tables for storing users, encrypting password, etc.
What you are trying to do is to use the new authentication system provided by OWIN, which replaces the old FormsAuthentication style.
To make it work, you have to create the cookie authentication. Like this:
public static class AuthConfig
{
public const string DefaultAuthType = "DefaultAppCookie";
public const string LoginPath = "/System/SignIn";
public static void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthType,
LoginPath = new PathString(LoginPath)
});
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier; //or whatever
}
}
In the login action:
var claims = new Claim[] {
new Claim(ClaimTypes.Name, "somename"),
new Claim(ClaimTypes.NameIdentifier, "someidentifier"),
new Claim("foo", "bar"),
};
ClaimsIdentity identity = new ClaimsIdentity(claims, AuthConfig.DefaultAuthType);
IAuthenticationManager authManager = Request.GetOwinContext().Authentication;
authManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, identity);
I think that should be enough to make it work in your app. A few days ago I answered a similar question MVC Authentication - Easiest Way, take a look, it might be helpful.
I recently have added Active Directory authentication, constructed ClaimsPrincipal myself and signed-in the same way you do.
And you are indeed missing .UseCookieAuthentication in your ConfigureAuth(IAppBuilder app)
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "MyAuthenticationName", // <-- this must match the AuthenticatioType name when you do sign-out
LoginPath = new PathString("/MyLoginPath"),
CookieName = "MyCookieName",
CookieHttpOnly = true,
});
}
And you don't need UseExternalSignInCookie.
Request.IsAuthenticated will be false with in the same request flow.
I think you still need to update the current security principal if you need to check IsAuthenticated for the request as authenticationManager.SignIn only validates the user against data store and sets the OWIN cookie which when sent back in subsequent request sets the security principal , usually a redirect takes care of this as in most cases there will be redirection in home page or something. If you still need to check with in the same request you can do something like below depending on your requirement
var claims = new Claim[] {
new Claim(ClaimTypes.Name, "somename"),
new Claim(ClaimTypes.NameIdentifier, "someidentifier"),
new Claim("foo", "bar"),
};
var identity = new ClaimsIdentity(claims,DefaultAuthenticationTypes.ApplicationCookie,
ClaimTypes.Name, ClaimTypes.Role);
var principal = new ClaimsPrincipal(identity);
System.Threading.Thread.CurrentPrincipal = principal;
if (System.Web.HttpContext.Current != null)
System.Web.HttpContext.Current.User = principal;
Hope this helps.

Prevent multiple logins

I am trying to block multiple logins with the same user in my application. My idea is to update the security stamp when user signin and add that as a Claim, then in every single request comparing the stamp from the cookie with the one in the database. This is how I've implemented that:
public virtual async Task<ActionResult> Login([Bind(Include = "Email,Password,RememberMe")] LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
SignInStatus result =
await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, false);
switch (result)
{
case SignInStatus.Success:
var user = UserManager.FindByEmail(model.Email);
var id = user.Id;
UserManager.UpdateSecurityStamp(user.Id);
var securityStamp = UserManager.FindByEmail(model.Email).SecurityStamp;
UserManager.AddClaim(id, new Claim("SecurityStamp", securityStamp));
Then in authentication configuration I've added
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)
{
var userManager = new UserManager<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;
}
}
});
As it shows I have tried to compare the claim from the cookie with the one in the database and reject the identity if they are not the same.
Now, each time the user signs in the security stamp gets updated but the value is different in user's cookie which I can't find out why? I am suspicious maybe it the new updated security stamp doesn't get stored in user's cookie?
The solution is somewhat more simple than you have started implementing. But the idea is the same: every time user logs in, change their security stamp. And this will invalidate all other login sessions. Thus will teach users not to share their password.
I have just created a new MVC5 application from standard VS2013 template and successfully managed to implement what you want to do.
Login method. You need to change the security stamp BEFORE you create auth cookie, as after the cookie is set, you can't easily update the values:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// check if username/password pair match.
var loggedinUser = await UserManager.FindAsync(model.Email, model.Password);
if (loggedinUser != null)
{
// change the security stamp only on correct username/password
await UserManager.UpdateSecurityStampAsync(loggedinUser.Id);
}
// do sign-in
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
This way every login will do an update on the user record with the new security stamp. Updating security stamp is only a matter of await UserManager.UpdateSecurityStampAsync(user.Id); - much simplier than you imagined.
Next step is to check for security stamp on every request. You already found the best hook-in point in Startup.Auth.cs but you again overcomplicated. The framework already does what you need to do, you need to tweak it slightly:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// other stuff
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(0), // <-- Note the timer is set for zero
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
The time interval is set for zero - means the framework on every request will compare user's security stamp with the database. If stamp in the cookie does not match the stamp in the database, user's auth-cookie is thrown out, asking them to logout.
However, note that this will bear an extra request to your database on every HTTP request from a user. On a large user-base this can be expensive and you can somewhat increase the checking interval to a couple minutes - will give you less requests to your DB, but still will carry your message about not sharing the login details.
Full source in github
More information in a blog-post
In the past I've used IAuthorizationFilter and static logged-in user collection to achieve this:
public static class WebAppData
{
public static ConcurrentDictionary<string, AppUser> Users = new ConcurrentDictionary<string, AppUser>();
}
public class AuthorisationAttribute : FilterAttribute, IAuthorizationFilter {
public void OnAuthorization(AuthorizationContext filterContext){
...
Handle claims authentication
...
AppUser id = WebAppData.Users.Where(u=>u.Key ==userName).Select(u=>u.Value).FirstOrDefault();
if (id == null){
id = new AppUser {...} ;
id.SessionId = filterContext.HttpContext.Session.SessionID;
WebAppData.Users.TryAdd(userName, id);
}
else
{
if (id.SessionId != filterContext.HttpContext.Session.SessionID)
{
FormsAuthentication.SignOut();
...
return appropriate error response depending is it ajax request or not
...
}
}
}
}
On logout:
WebAppData.Users.TryRemove(userName, out user)

ExpireTimeSpan ignored after regenerateIdentity / validateInterval duration in MVC Identity (2.0.1)

Been scratching my head all day on this one. I'm trying to set up "very long" login sessions in MVC Identity 2.0.1. (30 days).
I use the following cookie startup:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
SlidingExpiration = true,
ExpireTimeSpan = System.TimeSpan.FromDays(30),
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/My/Login"),
CookieName = "MyLoginCookie",
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
Which on the whole, works fine. The cookie is set 30 days hence, all looks good.
If I close browser and come back after "validateInterval" duration has passed (30mins here) I'm still logged in, however the cookie is now re-issued as "session" only (correct cookie name still)! The 30 day expiration is gone.
If I now close browser/reopen again I'm no longer logged in.
I have tested removing the "Provider" and all works as expected then, I can come back several hours later and I'm still logged in fine.
I read that it is best practice to use the stamp revalidation though, so am unsure how to proceed.
When the SecurityStampValidator fires the regenerateIdentity callback, the currently authenticated user gets re-signed in with a non-persistent login. This is hard-coded, and I don't believe there is any way to directly control it. As such, the login session will continue only to the end of the browser session you are running at the point the identity is regenerated.
Here is an approach to make the login persistent, even across identity regeneration operations. This description is based on using Visual Studio MVC ASP.NET web project templates.
First we need to have a way to track the fact that a login session is persistent across separate HTTP requests. This can be done by adding an "IsPersistent" claim to the user's identity. The following extension methods show a way to do this.
public static class ClaimsIdentityExtensions
{
private const string PersistentLoginClaimType = "PersistentLogin";
public static bool GetIsPersistent(this System.Security.Claims.ClaimsIdentity identity)
{
return identity.Claims.FirstOrDefault(c => c.Type == PersistentLoginClaimType) != null;
}
public static void SetIsPersistent(this System.Security.Claims.ClaimsIdentity identity, bool isPersistent)
{
var claim = identity.Claims.FirstOrDefault(c => c.Type == PersistentLoginClaimType);
if (isPersistent)
{
if (claim == null)
{
identity.AddClaim(new System.Security.Claims.Claim(PersistentLoginClaimType, Boolean.TrueString));
}
}
else if (claim != null)
{
identity.RemoveClaim(claim);
}
}
}
Next we need to make the "IsPersistent" claim when the user signs in requesting a persistent session. For example, your ApplicationUser class may have a GenerateUserIdentityAsync method which can be updated to take an isPersistent flag parameter as follows to make such a claim when needed:
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager, bool isPersistent)
{
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
userIdentity.SetIsPersistent(isPersistent);
return userIdentity;
}
Any callers of ApplicationUser.GenerateUserIdentityAsync will now need to pass in the isPersistent flag. For example, the call to GenerateUserIdentityAsync in AccountController.SignInAsync would change from
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent },
await user.GenerateUserIdentityAsync(UserManager));
to
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent },
await user.GenerateUserIdentityAsync(UserManager, isPersistent));
Lastly, the CookieAuthenticationProvider.OnValidateIdentity delegate used in the Startup.ConfigureAuth method needs some attention to preserve the persistence details across identity regeneration operations. The default delegate looks like:
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(20),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
This can be changed to:
OnValidateIdentity = async (context) =>
{
await SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(20),
// Note that if identity is regenerated in the same HTTP request as a logoff attempt,
// the logoff attempt will have no effect and the user will remain logged in.
// See https://aspnetidentity.codeplex.com/workitem/1962
regenerateIdentity: (manager, user) =>
user.GenerateUserIdentityAsync(manager, context.Identity.GetIsPersistent())
)(context);
// If identity was regenerated by the stamp validator,
// AuthenticationResponseGrant.Properties.IsPersistent will default to false, leading
// to a non-persistent login session. If the validated identity made a claim of being
// persistent, set the IsPersistent flag to true so the application cookie won't expire
// at the end of the browser session.
var newResponseGrant = context.OwinContext.Authentication.AuthenticationResponseGrant;
if (newResponseGrant != null)
{
newResponseGrant.Properties.IsPersistent = context.Identity.GetIsPersistent();
}
}
This bug is fixed in ASP.NET Identity 2.2. See https://aspnetidentity.codeplex.com/workitem/2319

Resources