question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Custom claims lost after sometime in AspNetCore Identity cookie

See original GitHub issue

Is 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):

  1. I add some claims during login (these claims come from some API call and not from Identity db, so I add them during login).

  2. I access them from Blazor components just fine.

  3. 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:

  1. 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>>();

  1. 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();
        }
    }
  1. 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;
            }
        }
    }
  1. 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:closed
  • Created 2 months ago
  • Comments:8 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
halter73commented, Jul 25, 2023

You can use the options pattern. So, the simplest way would be something like this:

builder.Services.Configure<SecurityStampValidatorOptions>(o => 
{
    o.OnRefreshingPrincipal = principalContxt =>
    {
        ClaimsIdentity? newIdentity = principalContxt.NewPrincipal?.Identities.First();
        AddCustomClaims(newIdentity);
        return Task.CompletedTask;
    };
});

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.

0reactions
affableashishcommented, Jul 28, 2023

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:

public class ConfigureSecurityStampOptions : IConfigureOptions<SecurityStampValidatorOptions>
{
	public void Configure(SecurityStampValidatorOptions options)
	{
		options.ValidationInterval = TimeSpan.FromMinutes(10); // Default interval is 30 minutes

		// When refreshing the principal, ensure custom claims that
		// might have been set with an external identity continue
		// to flow through to this new one.
		options.OnRefreshingPrincipal = refreshingPrincipal =>
		{
			ClaimsIdentity? newIdentity = refreshingPrincipal.NewPrincipal?.Identities.First();
			ClaimsIdentity? currentIdentity = refreshingPrincipal.CurrentPrincipal?.Identities.First();

			if (currentIdentity is not null && newIdentity is not null)
			{
				// Since this is refreshing an existing principal, we want to merge all claims.
				// Only work with claims in current identity that are not already present in the new identity with the same Type and Value.
				var currentClaimsNotInNewIdentity = currentIdentity.Claims.Where(c => !newIdentity.HasClaim(c.Type, c.Value));

				foreach (Claim claim in currentClaimsNotInNewIdentity)
				{
					newIdentity.AddClaim(claim);
				}
			}

			return Task.CompletedTask;
		};
	}
}

Step 3:

Register it in Program.cs

// To ensure custom claims are added to new identity when principal is refreshed.
builder.Services.ConfigureOptions<ConfigureSecurityStampOptions>();

Full Source Code

https://github.com/affableashish/blazor-server-auth/tree/feature/AddClaimsDuringLogin

Stackoverflow Answer:

https://stackoverflow.com/a/76790172/8644294

Read more comments on GitHub >

github_iconTop Results From Across the Web

Custom claims lost after some time in AspNetCore Identity ...
I've put all the details in the question below: c# - Custom claims lost after sometime in AspNetCore Identity cookie - Stack Overflow....
Read more >
Mapping, customizing, and transforming claims in ASP. ...
Learn how to map claims, do claims transformations, customize claims.
Read more >
Three(+1) ways to refresh the claims of a logged-in user
1. Update user claims via cookie event: This is a relatively easy way to update the user's claims when using is the standard...
Read more >
Use cookie authentication without ASP.NET Core Identity
Authentication cookies are allowed when a site visitor hasn't consented to data collection. For more information, see General Data Protection ...
Read more >
Adding Custom Claims when Logging In with Asp.Net Core ...
There's an easy way to add custom claims when an user logs in with Asp.Net Cores Cookie Authentication.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found