Search code examples
c#authentication.net-8.0asp.net-core-8

JWT generation not correct for REST client access


I have configured my ASP.NET Core 8 web app to use JWT tokens for authentication.

This is how it is configured (still in startup.cs):

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
   options.SaveToken = true;
   options.RequireHttpsMetadata = false;
   options.TokenValidationParameters = new TokenValidationParameters
   {
       ValidateIssuer = true,
       ValidateAudience = true,
       ValidateLifetime = true,
       ValidateIssuerSigningKey = true,
       ClockSkew = TimeSpan.Zero,

       ValidAudience = Configuration["Jwt:Audience"],
       ValidIssuer = Configuration["Jwt:Issuer"],
       IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])),
   };
});

This JWT token configuration works fine when navigating to views with corresponding controllers decorated with the Authorize attribute.

However, this web app also provides a REST API, where the client (.NET Framework 4.0 WPF app) uses bearer tokens on the request header for authentication. These tokens are also generated by the application in a TokenController.

The problem I am experiencing is that the generated tokens seem incorrect. Since the response content from an API call is the HTML of the redirect page (result of a 401 response), I am concluding that the authentication failed.

This is how tokens are generated:

    [AllowAnonymous]
    [HttpPost]
    public async Task<IActionResult> Index(LoginUserUser usr)
    {
        IActionResult response = Unauthorized();

        var user = await userManager.FindByNameAsync(usr.username);

        if (user != null && await userManager.CheckPasswordAsync(user, usr.password))
        {
            var userRoles = await userManager.GetRolesAsync(user);

            var authClaims = new List<Claim>
            {
                new(ClaimTypes.Name, user.UserName),
                new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            };

            foreach (var userRole in userRoles)
            {
                authClaims.Add(new Claim(ClaimTypes.Role, userRole));
            }

            var token = CreateToken(authClaims);
            var refreshToken = GenerateRefreshToken();

            user.RefreshToken = refreshToken;
            var refreshTokenValidityInDays = Convert.ToInt32(Configuration["Jwt:RefreshTokenValidityInDays"]);
            user.RefreshTokenExpiryTime = DateTime.Now.AddDays(refreshTokenValidityInDays);

            await userManager.UpdateAsync(user);

            response = Ok(new
            {
                access_token = new JwtSecurityTokenHandler().WriteToken(token)
            });
        }

        return response;
    }

    private JwtSecurityToken CreateToken(List<Claim> authClaims)
    {
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]));
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

        var result = new JwtSecurityToken(
          issuer: Configuration["Jwt:Issuer"],
          audience: Configuration["Jwt:Audience"],
          claims: authClaims,
          expires: DateTime.Now.AddMinutes(1440),
          signingCredentials: credentials);

        return result;
    }

For the moment, I assume there is a mismatch between the configuration and the token generation, but I could not isolate the problem or have a good debugging approach.

Update:

I have used the recommended jwt.ms website and the access token looks valid. Here is an example:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiaWdvckBpbm92YXRpdmF0ZWMuY29tIiwianRpIjoiODkzZTU4YmYtYTlmNC00Nzg3LTlmYTMtZjhhMzUxYWI4NTk3IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjpbIklubWVzIiwiQWRtaW4iLCJRdWFudGl0eSIsIlVzZXJNYW5hZ2VyIl0sImV4cCI6MTcyOTYyOTAxOCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNDQiLCJhdWQiOiJodHRwczovL2xvY2FsaG9zdDo0NDM0NCJ9.skP2c_vhpCuUxSL_ysTCdOMe-xgmZO6OkyoSzqFUH5Y

What it is not clear to me is what should go on the issuer and audience. At the moment I used the URL of my local iss web app, but not sure if or how this interfere with my current issue,as @Mason A suggested. I verified the signature is OK.

As a sanity check, I also verified the token generated is the same as used on the client bearer authentication header

I was also able to reproduce the issue replacing the client by Postman


Solution

  • The issuer should be set to a string, that represents who created/issued the token. In your case, when you create them yourself, you can call it "MyClientApplication" or what ever.

    The audience in the token typically repsents who is the accesstoken created for, it could be "MyBackendAPI", or "MyPaymentAPI" or "MyOrderAPI".

    The JwtBearer can use the issuer to check if the creator of the token is what I expect and the audience allows the API to check if the API is meant for me.

    Compare it to the hotel, the door wants to ensure the key card was created(issued) by my hotel and the cards audience is the door lock to my room. The audience could also be the "guest rooms" and not the conference rooms/area.

    enter image description here