Search code examples
c#asp.net-corejwtasp.net-core-identity

When a new access token is generated with a refresh token, ClaimTypes.NameIdentifier returns null


In my project, after logging in, an access token and a refresh token are generated for the user with JWT. When the access token expires, a request is made again with the refresh token and a new access token is generated.

So far, there is no problem. This process continues in the background without redirecting the user to the login page.

The code structure is as follows:

public class AuthorizedHttpClientHandler : DelegatingHandler
{
    private readonly IHttpClientFactory _clientFactory;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public AuthorizedHttpClientHandler(IHttpContextAccessor httpContextAccessor, IHttpClientFactory clientFactory)
    {
        _httpContextAccessor = httpContextAccessor;
        _clientFactory = clientFactory;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var accessToken = _httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(x => x.Type == "DirimOpsManagement")?.Value;
        var refreshToken = _httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(x => x.Type == "RefreshToken")?.Value;

        if (!string.IsNullOrWhiteSpace(accessToken))
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        }

        var response = await base.SendAsync(request, cancellationToken);

        if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && !string.IsNullOrWhiteSpace(refreshToken))
        {
            var newToken = await RefreshToken(refreshToken);

            if (newToken != null)
            {
                var claims = new List<Claim>
                {
                    new Claim("DirimOpsManagement", newToken.AccessToken),
                    new Claim("RefreshToken", newToken.RefreshToken)
                };

                var claimsIdentity = new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
                var authProperties = new AuthenticationProperties
                {
                    IsPersistent = true
                };

                var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

                await _httpContextAccessor.HttpContext.SignInAsync(JwtBearerDefaults.AuthenticationScheme, claimsPrincipal, authProperties);

                _httpContextAccessor.HttpContext.User = claimsPrincipal;

                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken.AccessToken);
                response = await base.SendAsync(request, cancellationToken);
            }
            else
            {
                await _httpContextAccessor.HttpContext.SignOutAsync(JwtBearerDefaults.AuthenticationScheme);
                _httpContextAccessor.HttpContext.Response.Redirect("/Login/Index");
                await _httpContextAccessor.HttpContext.Response.CompleteAsync();
            }
        }
        return response;
    }

    private async Task<JwtResponseModel> RefreshToken(string refreshToken)
    {
        var client = _clientFactory.CreateClient();
        var json = JsonConvert.SerializeObject(new { RefreshToken = refreshToken });
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await client.PutAsync("https://localhost:7125/api/RefreshToken", content);
        if (response.IsSuccessStatusCode)
        {
            var jsonData = await response.Content.ReadAsStringAsync();
            return System.Text.Json.JsonSerializer.Deserialize<JwtResponseModel>(jsonData, new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            });
        }
        return null;
    }
}

However, in some cases (like create operations), I need to access the user Id. I achieve this with the following code:

public class UserService : IUserService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string GetUserId => _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
}
[HttpPost]
public async Task<IActionResult> CreateInventory(CreateInventoryDto createInventoryDto)
{
    CreateInventoryValidator validator = new CreateInventoryValidator();
    var validationResult = validator.Validate(createInventoryDto);
    if (validationResult.IsValid)
    {
        var count = await _inventoryService.GetIsInventoryUniqueAsync(createInventoryDto.InventoryNo);

        if (count > 0)
        {
            return BadRequest("Demirbaş Sistemde Kayıtlıdır");
        }

        **var user = _userService.GetUserId;**
        createInventoryDto.CreatedUser = user;

        await _inventoryService.CreateInventoryAsync(createInventoryDto);
        return Json(new { success = true });
    }
    else
    {
        var errors = validationResult.Errors.Select(e => e.ErrorMessage).ToList();
        return BadRequest(new { error = errors });
    }
}

There is no problem after the initial login, but after a new access token is generated, ClaimTypes.NameIdentifier returns null.

How can I solve this issue? I can only access it with the initially generated access token. Keeping the access token's lifespan long is not secure, so I keep the refresh token's lifespan long.

I would appreciate your support.

Thank you.

Edit - Refresh Token Request Code

public async Task<RefreshTokenResponse> Handle(UpdateRefreshTokenCommand request, CancellationToken cancellationToken)
{
    var existingToken = await _unitOfWork.RefreshTokens.GetByTokenAsync(request.RefreshToken);

    if (existingToken == null)
    {
        throw new Exception("Refresh token not found.");
    }

    if (existingToken.Expires < DateTime.UtcNow)
    {
        throw new Exception("Refresh token expired.");
    }

    var user = await _userManager.FindByIdAsync(existingToken.UserID);

    if (user == null)
    {
        throw new Exception("User not found.");
    }

    var accessToken = _jwtTokenGenerator.GenerateToken(new JwtTokenRequest
    {
        Id = user.Id,
        Username = user.UserName,
        Email = user.Email
    });

    existingToken.Token = _jwtTokenGenerator.GenerateRefreshToken();
    // existingToken.Expires = DateTime.UtcNow.AddDays(_jwtTokenDefaults.RefreshTokenExpires);

    await _unitOfWork.RefreshTokens.UpdateAsync(existingToken);
    await _unitOfWork.SaveChangesAsync(cancellationToken);

    return new RefreshTokenResponse
    {
        AccessToken = accessToken.AccessToken,
        AccessTokenExpires = accessToken.Expires,
        RefreshToken = existingToken.Token,
        RefreshTokenExpires = existingToken.Expires
    };
}

Solution

  • Change your code like below, then ClaimTypes.NameIdentifier null issue will be fixed.

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var accessToken = _httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(x => x.Type == "DirimOpsManagement")?.Value;
        var refreshToken = _httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(x => x.Type == "RefreshToken")?.Value;
    
        if (!string.IsNullOrWhiteSpace(accessToken))
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        }
    
        var response = await base.SendAsync(request, cancellationToken);
    
        if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && !string.IsNullOrWhiteSpace(refreshToken))
        {
            var newToken = await RefreshToken(refreshToken);
    
            if (newToken != null)
            {
                var handler = new JwtSecurityTokenHandler();
                var jwtToken = handler.ReadJwtToken(newToken.AccessToken);
    
                var claims = new List<Claim>
                {
                    new Claim("DirimOpsManagement", newToken.AccessToken),
                    new Claim("RefreshToken", newToken.RefreshToken)
                };
    
                claims.AddRange(jwtToken.Claims);
    
                var claimsIdentity = new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
                var authProperties = new AuthenticationProperties
                {
                    IsPersistent = true
                };
    
                var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
    
                await _httpContextAccessor.HttpContext.SignInAsync(JwtBearerDefaults.AuthenticationScheme, claimsPrincipal, authProperties);
    
                _httpContextAccessor.HttpContext.User = claimsPrincipal;
    
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken.AccessToken);
                response = await base.SendAsync(request, cancellationToken);
            }
            else
            {
                await _httpContextAccessor.HttpContext.SignOutAsync(JwtBearerDefaults.AuthenticationScheme);
                _httpContextAccessor.HttpContext.Response.Redirect("/Login/Index");
                await _httpContextAccessor.HttpContext.Response.CompleteAsync();
            }
        }
        return response;
    }