Reputation: 4657
I have an ASP.NET MVC application and I'm using ASP.NET Identity 2. I have a weird problem. ApplicationUser.GenerateUserIdentityAsync
is getting called for each request browser makes to my website. I've added some Trace.WriteLine
and this is the result after removing IIS output:
IdentityConfig.Configuration called
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Content/bootstrap.css
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Scripts/modernizr-2.8.3.js
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Content/site.css
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Scripts/jquery-2.1.3.js
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Scripts/bootstrap.js
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Scripts/respond.js
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Scripts/script.js
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Glimpse.axd?n=glimpse_client&hash=8913cd7e
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Glimpse.axd?n=glimpse_metadata&hash=8913cd7e&callback=glimpse.data.initMetadata
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Glimpse.axd?n=glimpse_request&requestId=6171c2b0-b6e5-4495-b495-4fdaddbe6e8f&hash=8913cd7e&callback=glimpse.data.initData
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/Glimpse.axd?n=glimpse_sprite&hash=8913cd7e
ApplicationUser.GenerateUserIdentityAsync called url: http://localhost:54294/__browserLink/requestData/38254292a54f4595ad26158540adbb6a?version=2
while if I run a default MVC Application created by template, I'm getting this:
IdentityConfig.Configuration called
and only if I login, It'll call ApplicationUser.GenerateUserIdentityAsync
.
I've looked everywhere I thought it might be but I didn't find any result. I'm using (if it helps)
StructureMap 3
Elmah
Glimpse
ASP.NET MVC 5
EF6
ASP.NET Identity 2
Additional Info
I'm adding users directly into database without using a UserManage. I'm not sure if it makes any problems with Identity or not.
Update
I've dropped database and it didn't happen anymore. What is happening?
Update 2
It happened in my Google Chrome (I monitor SQL connections using glimpse) and after removing stored cookies, It didn't happen. Can logging in in other browsers cause this problem?
Update 3
Also log off - log in seems to solve the problem temporary.
Upvotes: 4
Views: 2277
Reputation: 68
I had the same problem and after digging in the source code and some detective work I found a solution. The problem is inside the SecurityStampValidator
, which is used as default OnValidateIdentity
handler. See the source code here. Interesting part:
var issuedUtc = context.Properties.IssuedUtc;
// Only validate if enough time has elapsed
var validate = (issuedUtc == null);
if (issuedUtc != null)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
validate = timeElapsed > validateInterval;
}
This part runs for each request and if validate
is true, then getUserIdCallback
and regenerateIdentityCallback
(visible in your trace output) are called. The problem here is that issuedUtc
is always the date when cookie was created, so validate
is always true when validateInterval
has passed. This explains the weird behavior you were experiencing. If validateInterval
is 10 minutes, validation logic will run for each request coming in 10 minutes and more after cookie was created (application deployed, cookies cleared, cookie reset when logging out and in again).
SecurityStampValidator
should make the decisions whether validate or not, basing on the previous validation date (or issued date when it's a first check), but it's not doing so. To make issuedUtc
date move forward there are 3 possible solutions:
validateInterval
, this means SingOut
and SignIn
. Similar solution here. This seems like a costly operation, especially if validateInterval
is set to only a couple of minutes.CookieAuthenticationOptions.SlidingExpiration
logic to have the cookie re-issued automatically. Explained very well in this post.If SlidingExpiration is set to true then the cookie would be re-issued on any request half way through the ExpireTimeSpan. For example, if the user logged in and then made a second request 16 minutes later the cookie would be re-issued for another 30 minutes. If the user logged in and then made a second request 31 minutes later then the user would be prompted to log in.
In my case (intranet application) users being logged out after 30 minutes of inactivity is unacceptable. I need to have the default ExpireTimeSpan
, which is 14 days. So the option here would be to implement some kind of ajax polling to extend the cookie life. Sounds like a lot of effort to accomplish this fairly simple scenario.
the last option, which I finally chose to use is to modify SecurityStampValidator
implementation to have the sliding validation approach. Example code below. Remember to replace SecurityStampValidator
with SlidingSecurityStampValidator
in Startup.Auth.cs. I added IdentityValidationDates
dictionary to the original implementation to store the validation dates for each user and then I'm using it when checking if validation is needed.
public static class SlidingSecurityStampValidator
{
private static readonly IDictionary<string, DateTimeOffset> IdentityValidationDates = new Dictionary<string, DateTimeOffset>();
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>
{
if (getUserIdCallback == null)
{
throw new ArgumentNullException(nameof(getUserIdCallback));
}
return async context =>
{
var currentUtc = DateTimeOffset.UtcNow;
if (context.Options != null && context.Options.SystemClock != null)
{
currentUtc = context.Options.SystemClock.UtcNow;
}
var issuedUtc = context.Properties.IssuedUtc;
// Only validate if enough time has elapsed
var validate = issuedUtc == null;
if (issuedUtc != null)
{
DateTimeOffset lastValidateUtc;
if (IdentityValidationDates.TryGetValue(context.Identity.Name, out lastValidateUtc))
{
issuedUtc = lastValidateUtc;
}
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
validate = timeElapsed > validateInterval;
}
if (validate)
{
IdentityValidationDates[context.Identity.Name] = currentUtc;
var manager = context.OwinContext.GetUserManager<TManager>();
var userId = getUserIdCallback(context.Identity);
if (manager != null && userId != null)
{
var user = await manager.FindByIdAsync(userId);
var reject = true;
// Refresh the identity if the stamp matches, otherwise reject
if (user != null && manager.SupportsUserSecurityStamp)
{
var securityStamp = context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType);
if (securityStamp == await manager.GetSecurityStampAsync(userId))
{
reject = false;
// Regenerate fresh claims if possible and resign in
if (regenerateIdentityCallback != null)
{
var identity = await regenerateIdentityCallback.Invoke(manager, user);
if (identity != null)
{
// Fix for regression where this value is not updated
// Setting it to null so that it is refreshed by the cookie middleware
context.Properties.IssuedUtc = null;
context.Properties.ExpiresUtc = null;
context.OwinContext.Authentication.SignIn(context.Properties, identity);
}
}
}
}
if (reject)
{
context.RejectIdentity();
context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
}
}
}
};
}
}
Upvotes: 4
Reputation: 2610
There are two possible issues that are causing this the reset:
issuedUtc
property on the cookie is null
)In your startup.cs
class you will find the Cookie Authentication configuration. And inside the configuration is a function delegate that should be set like the following:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
...
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
...
}
}
Check to see that your validateInterval
is not set to a low value. In the case above there will be a database ( user.GenerateUserIdentityAsync
) call 30 minutes after a valid cookie was issued. In your case it maybe set to a low value like every second.
If you are using the logout everywhere functionality (security stamp) a change to the validateInterval
will allow the cookie to remain valid until the OnValidateIdentity
function is called.
Upvotes: 0