Search code examples
identityserver4

identityserver4 - ArgumentNullException: Value cannot be null


I am using IdentityServer4 and I keep having an issue when the access token has expired. If the user tries to do something like logout after this happens then it gives the error below. But this also happens when you try to login again as well. The only way to fix it is to clear cache and cookies from the browser. I understand the error message but I cant find out where to make the checks for the null because I think the UserClaimsFactory.cs is a protected resource in the nuget package so there is nothing I can do.

System.ArgumentNullException: Value cannot be null.
Parameter name: value
   at System.Security.Claims.Claim..ctor(String type, String value, String valueType, String issuer, String originalIssuer, ClaimsIdentity subject, String propertyKey, String propertyValue)
   at System.Security.Claims.Claim..ctor(String type, String value)
   at Microsoft.AspNetCore.Identity.UserClaimsPrincipalFactory`1.<GenerateClaimsAsync>d__10.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.AspNetCore.Identity.UserClaimsPrincipalFactory`2.<GenerateClaimsAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.AspNetCore.Identity.UserClaimsPrincipalFactory`1.<CreateAsync>d__9.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at IdentityServer4.AspNetIdentity.UserClaimsFactory`1.<CreateAsync>d__3.MoveNext() in C:\local\identity\server4\AspNetIdentity\src\IdentityServer4.AspNetIdentity\UserClaimsFactory.cs:line 28
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.AspNetCore.Identity.SignInManager`1.<CreateUserPrincipalAsync>d__25.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.AspNetCore.Identity.SecurityStampValidator`1.<ValidateAsync>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler.<HandleAuthenticateAsync>d__20.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.<AuthenticateAsync>d__47.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.AspNetCore.Authentication.AuthenticationService.<AuthenticateAsync>d__10.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.<Invoke>d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware.<Invoke>d__7.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at IdentityServer4.Hosting.BaseUrlMiddleware.<Invoke>d__3.MoveNext() in C:\local\identity\server4\IdentityServer4\src\IdentityServer4\Hosting\BaseUrlMiddleware.cs:line 36
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.<Invoke>d__7.MoveNext()

This is my configuration....

public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<IISOptions>(iis =>
            {
                iis.AuthenticationDisplayName = "Windows";
                iis.AutomaticAuthentication = false;
            });

        services.AddDbContext<UserIdentityDbContext>(builder =>
            builder.UseSqlServer(Configuration.GetConnectionString("IDPDatabaseConnection"), a => a.MigrationsAssembly("SMI.IDP")));

        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<UserIdentityDbContext>()
            .AddDefaultTokenProviders();

        services.AddSingleton(typeof(ILocalActiveDirectoryService), typeof(AmericasActiveDirectoryService));
        services.AddSingleton(typeof(IIdentityServerUserStore<ApplicationUser>), typeof(UsersRepository));
        services.AddScoped<ClaimsService>();
        services.AddScoped<UsersRepository>();

        services.AddMvc();

        var idsrvBuilder = services.AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddConfigurationStore(options =>
        {
            options.ConfigureDbContext = builder =>
                builder.UseSqlServer(Configuration.GetConnectionString("IDPDatabaseConnection"),
                    sql => sql.MigrationsAssembly(_migrationsAssembly));
        })
        .AddOperationalStore(options =>
        {
            options.ConfigureDbContext = builder =>
                builder.UseSqlServer(Configuration.GetConnectionString("IDPDatabaseConnection"),
                    sql => sql.MigrationsAssembly(_migrationsAssembly));

            // this enables automatic token cleanup. this is optional.
            options.EnableTokenCleanup = true;
            options.TokenCleanupInterval = 30;
        })
        .AddAspNetIdentity<ApplicationUser>();

        idsrvBuilder.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>();
        idsrvBuilder.AddProfileService<ProfileService>();
    }

Solution

  • After thorough investigation of this event, I've found out the UserClaimsPrincipalFactory is the cause of the issue here:

    enter image description here

    You will have to override the GetUserIdAsync of your own UserManager in order to make the default factory functions to work properly.

    Here's a sample class for you to kick it off with.

    using System;
    using System.Collections.Generic;
    using System.Security.Claims;
    using System.Security.Principal;
    using System.Threading.Tasks;
    using IdentityModel;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.Extensions.Options;
    using Nozomi.Base.Identity.Models.Identity;
    using Nozomi.Service.Identity.Managers;
    
    namespace Nozomi.Service.Identity.Factories
    {
        public class NozomiUserClaimsPrincipalFactory: UserClaimsPrincipalFactory<User, Role>
        {
            public new NozomiUserManager UserManager;
            public new RoleManager<Role> RoleManager;
    
            public NozomiUserClaimsPrincipalFactory(NozomiUserManager userManager, RoleManager<Role> roleManager, 
                IOptions<IdentityOptions> options) : base(userManager, roleManager, options)
            {
                UserManager = userManager;
                RoleManager = roleManager;
            }
    
            /// <summary>
            /// Creates a <see cref="T:System.Security.Claims.ClaimsPrincipal" /> from an user asynchronously.
            /// </summary>
            /// <param name="user">The user to create a <see cref="T:System.Security.Claims.ClaimsPrincipal" /> from.</param>
            /// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous creation
            /// operation, containing the created <see cref="T:System.Security.Claims.ClaimsPrincipal" />.</returns>
            public override async Task<ClaimsPrincipal> CreateAsync(User user)
            {
                var principal = await base.CreateAsync(user);
                var identity = (ClaimsIdentity)principal.Identity;
    
                var claims = new List<Claim>
                {
                    new Claim(JwtClaimTypes.Role, "user")
                };
    
                identity.AddClaims(claims);
                return principal;
            }
    
            /// <summary>Generate the claims for a user.</summary>
            /// <param name="user">The user to create a <see cref="T:System.Security.Claims.ClaimsIdentity" /> from.</param>
            /// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous creation operation, containing the created <see cref="T:System.Security.Claims.ClaimsIdentity" />.</returns>
            protected override async Task<ClaimsIdentity> GenerateClaimsAsync(User user)
            {
                try
                {
                    var userId = await UserManager.GetUserIdAsync(user);
                    var userNameAsync = await UserManager.GetUserNameAsync(user);
                    var id = new ClaimsIdentity("Identity.Application", 
                        this.Options.ClaimsIdentity.UserNameClaimType, this.Options.ClaimsIdentity.RoleClaimType);
                    id.AddClaim(new Claim(this.Options.ClaimsIdentity.UserIdClaimType, userId));
                    id.AddClaim(new Claim(this.Options.ClaimsIdentity.UserNameClaimType, userNameAsync));
                    ClaimsIdentity claimsIdentity;
                    if (this.UserManager.SupportsUserSecurityStamp)
                    {
                        claimsIdentity = id;
                        string type = this.Options.ClaimsIdentity.SecurityStampClaimType;
                        claimsIdentity.AddClaim(new Claim(type, await this.UserManager.GetSecurityStampAsync(user)));
                        claimsIdentity = (ClaimsIdentity) null;
                        type = (string) null;
                    }
                    if (this.UserManager.SupportsUserClaim)
                    {
                        claimsIdentity = id;
                        claimsIdentity.AddClaims((IEnumerable<Claim>) await this.UserManager.GetClaimsAsync(user));
                        claimsIdentity = (ClaimsIdentity) null;
                    }
                    return id;
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                    return new ClaimsIdentity();
                }
            }
        }
    }
    

    Make sure to symlink your custom objects in the constructor else it won't work.

    Within your UserStore class:

    /// <summary>
            /// Gets the user identifier for the specified <paramref name="user" />.
            /// </summary>
            /// <param name="user">The user whose identifier should be retrieved.</param>
            /// <param name="cancellationToken">The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
            /// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation, containing the identifier for the specified <paramref name="user" />.</returns>
            public override Task<string> GetUserIdAsync(User user, CancellationToken cancellationToken)
            {
                try
                {
                    if (cancellationToken != null)
                        cancellationToken.ThrowIfCancellationRequested();
    
                    if (user == null)
                        throw new ArgumentException(nameof(user));
    
                    var res = _unitOfWork.GetRepository<User>().Get(u =>
                            u.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)
                            || user.UserName.Equals(user.UserName, StringComparison.InvariantCultureIgnoreCase))
                        .Select(u => u.Id)
                        .SingleOrDefault();
    
                    if (res == null)
                        throw new ArgumentOutOfRangeException(nameof(user));
    
                    return Task.FromResult(res.ToString());
                }
                catch (Exception ex)
                {
                    return Task.FromResult("-1");
                }
            }