Custom claims lost after sometime in AspNetCore Identity cookie
See original GitHub issueIs there an existing issue for this?
- I have searched the existing issues
Describe the bug
I see a question that was already asked years ago that explains a problem related to losing custom claims while using AspNet Identity. The solution mentioned there unfortunately doesn’t work for me as I’m using AspNet Core Identity on a .NET 6 Blazor Server app.
The issue is similar (explained in points below):
-
I add some claims during login (these claims come from some API call and not from Identity db, so I add them during login).
-
I access them from Blazor components just fine.
-
It works fine 30% of the time, but for 70% of the time, the cookie loses custom claims that I added during login and my app runs into issues. I’m not even able to figure out when those claims get lost as that’s not happening during
RevalidationInterval
either as I tested it with a TimeSpan of 1 minute and it worked well for at least 5 minutes when I tested it multiple times. Searched up bunch of answers and found no proper answer for AspNet Core Identity.
This is how my code looks like:
- Identity setup in Program.cs
builder.Services
.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
// Set Password options here if you'd like:
options.Password.RequiredLength = 6;
})
.AddRoles<IdentityRole>()
.AddUserManager<ADUserManager<IdentityUser>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<ApplicationUser>>();
- Adding claims during Login in Login.cshtml.cs
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
if (!ModelState.IsValid) return Page();
try
{
var adLoginResult = ADHelper.ADLogin(Input.Username, Input.Password);
// Use adLoginResult data to populate custom claims here
// Set additional info about the user using empTimeId and other custom claims
var customClaims = new[]
{
new Claim("EmployeeTimeId", adLoginResult.TimeId)
};
// SignIn the user now
await _signInManager.SignInWithClaimsAsync(user, Input.RememberMe, customClaims);
return LocalRedirect(returnUrl);
}
catch (Exception ex)
{
ModelState.AddModelError(string.Empty, $"Login Failed. Error: {ex.Message}.");
return Page();
}
}
- Revalidation method in RevalidatingIdentityAuthenticationStateProvider.cs
public class RevalidatingIdentityAuthenticationStateProvider<TUser>
: RevalidatingServerAuthenticationStateProvider where TUser : class
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IdentityOptions _options;
public RevalidatingIdentityAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> optionsAccessor)
: base(loggerFactory)
{
_scopeFactory = scopeFactory;
_options = optionsAccessor.Value;
}
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(1); // More frequent for ease of testing
protected override async Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken)
{
//Get the user manager from a new scope to ensure it fetches fresh data
var scope = _scopeFactory.CreateScope();
try
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
return await ValidateSecurityTimeStampAsync(userManager, authenticationState.User);
}
finally
{
if(scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
private async Task<bool> ValidateSecurityTimeStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if(user == null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
}
- Retrieving auth information
public class UserInfoService
{
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
public UserInfoService(AuthenticationStateProvider authenticationStateProvider, IDbContextFactory<ApplicationDbContext> dbContextFactory)
{
_authenticationStateProvider = authenticationStateProvider;
_dbContextFactory = dbContextFactory;
}
public async Task<UserInfoFromAuthState?> GetCurrentUserInfoFromAuthStateAsync()
{
var userInfo = new UserInfoFromAuthState();
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
if (authState == null ||
authState.User == null ||
authState.User.Identity == null ||
!authState.User.Identity.IsAuthenticated)
{
return null;
}
userInfo.UserName = authState.User.Identity.Name!;
// This comes out to be null after sometime a user has logged in
userInfo.EmployeeTimeId = int.TryParse(authState.User.FindFirstValue("EmployeeTimeId", out var timeId) ? timeId : null;
return userInfo;
}
}
This is where I face problem when I get null on my custom claim: "EmployeeTimeId"
.
Expected Behavior
Documented way of how to prevent customs claims from disappearing from the Identity cookie randomly.
Steps To Reproduce
No response
Exceptions (if any)
No response
.NET Version
No response
Anything else?
No response
Issue Analytics
- State:
- Created 2 months ago
- Comments:8 (4 by maintainers)
You can use the options pattern. So, the simplest way would be something like this:
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-configuration?view=aspnetcore-7.0#isecuritystampvalidator-and-signout-everywhere
And here’s an example usage from Umbraco-CMS.
Thank you @halter73. This is how I wrote my answer:
Step 1:
Add a new class under Identity folder called
ConfigureSecurityStampOptions.cs
(or anywhere you’d like):Step 2:
Content of
ConfigureSecurityStampOptions.cs
should be:Step 3:
Register it in
Program.cs
Full Source Code
https://github.com/affableashish/blazor-server-auth/tree/feature/AddClaimsDuringLogin
Stackoverflow Answer:
https://stackoverflow.com/a/76790172/8644294