2

I am stuck with the following situation and would very much appreciate any help:

I am using JSON Web Tokens (jwtbearer) for authentication and authorization in my User Interface (Blazor Server App) to access my Web API (Asp.net core 5.0).

I tried to return data from the my API filtered by UserId (which I also add as NameIdentifier when creating the token). Then I noticed, that the Claims of the ClaimsPrincipal returned from ControllerBase.User when hitting the controller are different from my token.

I have two NameIdentifier Claims, and the "sub" is missing. Rather, it looks like the sub has been changed to a second NameIdentifier.

Debugging Screenshot - Claims of this.User in Controller

However, it looks like the payload of the token, is correct, when I send with the request. This is the payload of my token:

{
  "sub": "admin@caliprog.com",
  "jti": "15c23576-9f5b-4f81-a985-ab236830c7b5",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "9b437d6c-8fb8-455d-87f5-986d36b26dcf",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Administrator",
  "exp": 1625472724,
  "iss": "test.com",
  "aud": "test.com"
}

Obviously I am making a mistake somewhere, but I have no idea where the error lies. It would be great if someone could point me in the direction. I am happy to do more research on my own, but I don't know, where to start.

Here are some of the code parts that might be relevant, please let me know, if I am missing anything crucial:

My GetRequest (it works, but I would love to replace this.User.Claims.ToList()[2].Value by var userId = this.User.FindFirst(ClaimTypes.NameIdentifier).Value;)

    [HttpGet]
    public async Task<IActionResult> GetWorkouts()
    {
        try
        {
            _logger.LogInfo(LogMessages.AttemptMsg, ControllerContext);

            var userId = this.User.Claims.ToList()[2].Value;
            var workouts = await _workoutRepository.FindAllByUser(userId);
            var response = _mapper.Map<IList<WorkoutDTO>>(workouts);
            _logger.LogInfo(LogMessages.SuccessActionMsg,ControllerContext);
            return Ok(response);
        }
        catch (Exception e)
        {

            return _logger.LogInternalErrorResult(e, ControllerContext);
        }
    }

UsersController -> Login and Generate Token:

    [Route("login")]
    [AllowAnonymous]
    [HttpPost]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    public async Task<IActionResult> Login([FromBody] UserDTO userDTO)
    {   
        try
        {
            var username = userDTO.EmailAddress;
            var password = userDTO.Password;

            _logger.LogInfo(string.Concat(LogMessages.AttemptMsg, $"With username: {username}."), ControllerContext);

            var result = await _signInManager.PasswordSignInAsync(username, password, false, false);

            if (result.Succeeded)
            {
                var user = await _userManager.FindByNameAsync(username);
                var tokenstring = await GenerateJSONWebToken(user);
                _logger.LogInfo(LogMessages.SuccessActionMsg, ControllerContext);
                return Ok(new { token = tokenstring});
            }
            _logger.LogInfo(string.Concat($"{username}: ", LogMessages.NotAuthenticated), ControllerContext);
            return Unauthorized(userDTO);
        }
        catch (Exception e)
        {
            return _logger.LogInternalErrorResult(e, ControllerContext);
        }
    }

    private async Task<string> GenerateJSONWebToken(IdentityUser user)
    {
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
        var claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Sub,user.Email),
            new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString()),
            new Claim(ClaimTypes.NameIdentifier,user.Id)
        };
        var roles = await _userManager.GetRolesAsync(user);
        claims.AddRange(roles.Select(r => new Claim(ClaimsIdentity.DefaultRoleClaimType, r)));

        var token = new JwtSecurityToken(
            _config["Jwt:Issuer"],
            _config["Jwt:Issuer"],
            claims,
            null,
            expires: DateTime.Now.AddHours(2),
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

UI Base Repository

    public async Task<IList<T>> Get(string url)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, url);

        var client = _client.CreateClient();
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("bearer", await GetBearerToken());

        HttpResponseMessage response = await client.SendAsync(request);

        if (response.StatusCode == System.Net.HttpStatusCode.OK)
        {
            var content = await response.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject<List<T>>(content);
        }

        return null;
    }


    private async Task<string> GetBearerToken()
    {
        return await _localStorage.GetItemAsync<string>("authToken");
    }

API AuthenticationStateProvider

    public async Task LoggedIn()
    {
        var savedToken = await _localStorage.GetItemAsync<string>("authToken");
        var tokenContent = _tokenHandler.ReadJwtToken(savedToken);
        var claims = ParseClaims(tokenContent);
        var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"));
        var authState = Task.FromResult(new AuthenticationState(user));
        NotifyAuthenticationStateChanged(authState);
    }

    public void LoggedOut()
    {
        var nobody = new ClaimsPrincipal(new ClaimsIdentity());
        var authState = Task.FromResult(new AuthenticationState(nobody));
        NotifyAuthenticationStateChanged(authState);
    }

    private static IList<Claim> ParseClaims(JwtSecurityToken tokenContent)
    {
        var claims = tokenContent.Claims.ToList();
        claims.Add(new Claim(ClaimTypes.Name, tokenContent.Subject));
        return claims;
    } 

Here are some of the project details for reference:

API
Microsoft.AspNetCore.Authentication.JwtBearer Version="5.0.5"
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore Version="5.0.5"
Microsoft.AspNetCore.Identity.EntityFrameworkCore Version="5.0.5"
Microsoft.AspNetCore.Identity.UI Version="5.0.4"
Microsoft.AspNetCore.Mvc.NewtonsoftJson Version="5.0.6"

UI
Blazored.LocalStorage Version="4.0.0"
Newtonsoft.Json Version="13.0.1"
System.IdentityModel.Tokens.Jwt Version="6.11.0"

Brocco-Lee
  • 23
  • 5

1 Answers1

7

Within your Startup.cs file you'll need to change your ConfigureServices method.

services.AddAuthentication(auth =>
            {
                auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(token =>
            {
                // ... Your extra options

                // Don't map the in bound claims.
                token.MapInboundClaims = false;
            });

This will preserve all the claims as they come in to your API.

Kevin Smith
  • 13,746
  • 4
  • 52
  • 77
  • 1
    Thank you so much! I used your code and it worked immediately. Awesome. The issue was in one of the few code sections, that I left unmentioned in my question... – Brocco-Lee Jul 05 '21 at 11:06
  • @Brocco-Lee bumped into this a few times... I should really blog about it. – Kevin Smith Jul 05 '21 at 11:38
  • 2
    I would definitely read the blog post! In case you do, you could consider including some explanation, what the first lamda expression in `.AddAuthentication` is doing.In my first version I only had `services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)` and it worked. I would be interested to learn, what the differences are. And I might not be alone :) – Brocco-Lee Jul 05 '21 at 12:07
  • setting the MapInboundClaims to false breaks Role based authorization (endpoints return 403 even when the required role is present in the token), do you have a workaround for that? – fbede Jan 20 '22 at 14:09
  • 1
    @fbede If you set the Role Claim type to your own role claim that that is coming in then that should sort your problem `token.TokenValidationParameters.RoleClaimType = "https://my.company/claims/role";` – Kevin Smith Jan 20 '22 at 21:13