2

I have a minimal api here. It creates me a JWT for authorization, and then it tests it. using the [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] attribute.

app.MapPost("/login", [AllowAnonymous]
    async (HttpContext http, ITokenService tokenService, IUserRepositoryService userRepositoryService) =>
    {
        var userLogin = await http.Request.ReadFromJsonAsync<UserModel>();
        var userDto = userRepositoryService.GetUser(userLogin);
        if (userDto == null)
        {
            http.Response.StatusCode = 401;
            return;
        }
        
        var token = tokenService.BuildToken(builder.Configuration["Jwt:Key"], builder.Configuration["Jwt:Issuer"],
            builder.Configuration["Jwt:Audience"], userDto);
        await http.Response.WriteAsJsonAsync(new { Token = token });
    });

app.MapGet("/secretAction",
    (Func<string>)([Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]() => "Action Succeeded")
);

The first method works just fine. A post to the login endpoint with a json string containing the login and password returns to me a jwt.

The second endpoint is always returning unauthorized.

I have been back and forth with this trying to understand why its not able to parse the token that it creates.

Any help would be greatly appreciated.

appsettings.json

"Jwt": {
    "Key": "this-is-the-secret",
    "Issuer": "https://jwtauth.example.com",
    "Audience": "api1"
  }

program.cs

using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ITokenService>(new TokenService());
builder.Services.AddSingleton<IUserRepositoryService>(new UserRepositoryService());

builder.Services.AddAuthorization();

var key = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]); 
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opt =>
    {
        opt.RequireHttpsMetadata = false;
        opt.SaveToken = true;
        opt.TokenValidationParameters = new TokenValidationParameters
        {
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuerSigningKey = true,
            
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidateIssuer = false,
             
            ValidAudience = builder.Configuration["Jwt:Audience"],
            ValidateAudience = true,
            
            ValidateLifetime = true,
        };
    });

await using var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapGet("/",
    (Func<string>)(() =>
        "Test JWT Authentication using Minimalist Web API .net 6. <br> /login UserName: user1, Password: test  <br>  /secretAction  "));

app.MapPost("/login", [AllowAnonymous]
    async (HttpContext http, ITokenService tokenService, IUserRepositoryService userRepositoryService) =>
    {
        var userLogin = await http.Request.ReadFromJsonAsync<UserModel>();
        var userDto = userRepositoryService.GetUser(userLogin);
        if (userDto == null)
        {
            http.Response.StatusCode = 401;
            return;
        }
        
        var token = tokenService.BuildToken(builder.Configuration["Jwt:Key"], builder.Configuration["Jwt:Issuer"],
            builder.Configuration["Jwt:Audience"], userDto);
        await http.Response.WriteAsJsonAsync(new { Token = token });
    });

app.MapGet("/secretAction",
    (Func<string>)([Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]() => "Action Succeeded")
);

await app.RunAsync();

public record UserDto(string UserName, string Password);

public record UserModel
{
    [Required] public string UserName { get; set; }

    [Required] public string Password { get; set; }
}

public interface IUserRepositoryService
{
    UserDto GetUser(UserModel userModel);
}

public class UserRepositoryService : IUserRepositoryService
{
    private List<UserDto> _users => new()
    {
        new("User1", "test"),
    };

    public UserDto GetUser(UserModel userModel)
    {
        return _users.FirstOrDefault(x =>
            string.Equals(x.UserName, userModel.UserName) && string.Equals(x.Password, userModel.Password));
    }
}

public interface ITokenService
{
    string BuildToken(string key, string issuer, string audience, UserDto user);
}

public class TokenService : ITokenService
{
    private TimeSpan ExpiryDuration = new TimeSpan(0, 30, 0);

    public string BuildToken(string key, string issuer, string audience, UserDto user)
    {
        var keyBytes = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
        
        var claims = new[]
        {
            new Claim(ClaimTypes.Name, user.UserName),
            new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString())
        };

        var descriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(claims),
            Issuer = issuer,
            Audience = audience,
            SigningCredentials = new SigningCredentials(keyBytes, SecurityAlgorithms.HmacSha256Signature),
            IssuedAt = DateTime.Now,
            NotBefore = DateTime.Now,
            Expires = DateTime.Now.AddDays(1)
        };

        var jwtHandler = new JwtSecurityTokenHandler();
        var token = jwtHandler.CreateToken(descriptor);
        return jwtHandler.WriteToken(token);

        // alternatively
        // return jwtHandler.CreateEncodedJwt(descriptor);
    }


    

To test it.

const string jwtUrl = "https://localhost:7080/login";
var content = new StringContent("{\"UserName\" : \"User1\",\"Password\" : \"test\"}", Encoding.UTF8, "application/json");
var httpResponseMessage  =  await client.PostAsync(jwtUrl,content);
var jwt = await httpResponseMessage.Content.ReadAsStringAsync();

var queueMessage = JsonSerializer.Deserialize<TokenResponse>(jwt);

Console.WriteLine(queueMessage.token);

client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", queueMessage.token);

const string protectedUrl = "https://localhost:7080/secretAction";
var result = await client.GetStringAsync(protectedUrl);

Console.WriteLine(result);

Logs

Here is a JWT just created.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiVXNlcjEiLCJuYW1laWQiOiI2OWJmOTg5Ni0wYTIyLTQ1N2UtODkyMy00ZTM4MGQzMTEyNTkiLCJuYmYiOjE2NjcyOTA5NTgsImV4cCI6MTY2NzM3NzM1OCwiaWF0IjoxNjY3MjkwOTU4LCJpc3MiOiJodHRwczovL2p3dGF1dGguZXhhbXBsZS5jb20i LCJhdWQiOiJhcGkxIn0.aUxhJuOrNOHeId6vHpHe1ZqnuC2MJ4TaYi577Cc37oU

The logs from trying to access the secretAction sending the jwt as a bearer token.

System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized). at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode() at System.Net.Http.HttpClient.GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken) at Program.$(String[] args) in C:\Development\FreeLance\Glassix\asp-net-core-auth-with-self-generated-jwt\ConsoleApp1\Program.cs:line 27

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449

1 Answers1

1

So with some hints from friends on twitter. and Khellang I added event logging to the error

 opt.Events = new JwtBearerEvents()
    {
        OnAuthenticationFailed = context =>
        {
            var err = context.Exception.ToString();
            
            return context.Response.WriteAsync(err);
        }
    };

This lead to the error message

Method not found: 'Void Microsoft.IdentityModel.Tokens.InternalValidators.ValidateLifetimeAndIssuerAfterSignatureNotValidatedJwt

Followed by this question on Stack Unauthorized (Invalid Token) when authenticating with JWT Bearer Token after update to .NET 6

After installing the recommended package System.IdentityModel.Tokens.Jwt everything magically works.

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449