skip to Main Content

When authenticating through JWT on ASP.NET Core 8 Web API, it always throws this error:

Bearer error="invalid_token"

regardless of what I do.

I have checked the token through jwt.io, where everything looks normal.

I have also checked the Chrome network tab, where everything looks normal.

I have straight up no idea, since I followed a YouTube tutorial where everything worked, and as I am quite new to ASP.NET Core.

Repo: https://github.com/LarsSK06/Home-Server.Backend/tree/dev

Program.cs:

using HomeServer.Data;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Filters;
using Microsoft.IdentityModel.Tokens;
using System.Text;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();

builder.Services.AddSwaggerGen(options => {
    options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme{
        In = ParameterLocation.Header,
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey
    });
    
    options.OperationFilter<SecurityRequirementsOperationFilter>();
});

builder.Services.AddSingleton<MongoDBService>();

builder.Services.AddCors(options => {
    options.AddDefaultPolicy(
        policy =>
            policy.WithOrigins("http://localhost:3000")
    );
});

builder.Services.AddAuthentication().AddJwtBearer(options => {
    options.TokenValidationParameters = new TokenValidationParameters{
        ValidateIssuerSigningKey = true,
        ValidateAudience = false,
        ValidateIssuer = false,
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(
                builder.Configuration.GetSection("AppSettings:Token").Value!
            )
        )
    };
});

WebApplication app = builder.Build();

if(app.Environment.IsDevelopment()){
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

appsettings.json:

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    },
    "AllowedHosts": "*",
    "ConnectionStrings": {
        "DatabaseConnection": "mongodb://localhost:27017/dev"
    },
    "AppSettings": {
        "Token": "jdiwlpmfpn210dff1kg92wagwagagrgrgrddgdgdrdrrggrdwdwafawfwafwafawfafawfffwwf"
    }
}

Users.cs (API controller for Users/ endpoint

using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using HomeServer.Models;
using HomeServer.Data;
using HomeServer.Utilities;
using Microsoft.AspNetCore.Authorization;

namespace HomeServer.Controllers;

[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
    private readonly IMongoCollection<User>? _users;
    private readonly IConfiguration _config;

    public UsersController(MongoDBService mongoDBService, IConfiguration configuration)
    {
        _users = mongoDBService.Database?.GetCollection<User>("users");
        _config = configuration;
    }

    [HttpGet, Authorize]
    public async Task<ActionResult<IEnumerable<PublicUser>>> GetUsers()
    {
        if (_users is null)
            return NotFound();

        FilterDefinition<User>? filter = FilterDefinition<User>.Empty;
        IAsyncCursor<User>? cursor = await _users.FindAsync(filter);
        List<User>? users = await cursor.ToListAsync();
        List<PublicUser> publicUsers = [];

        foreach(User i in users)
            publicUsers.Add(i.ToPublic());

        return Ok(publicUsers);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<PublicUser>> GetUser(int id)
    {
        if (_users is null)
            return NotFound();
        
        FilterDefinition<User>? filter = Builders<User>.Filter.Eq(i => i.Id, id);
        IAsyncCursor<User>? cursor = await _users.FindAsync(filter);
        User? first = await cursor.FirstOrDefaultAsync();

        if (first is null)
            return NotFound();

        return first.ToPublic();
    }

    [HttpPost]
    public async Task<ActionResult<PublicUser>> CreateUser(MutableUser data)
    {
        if (_users is null)
            return NotFound();
        
        FilterDefinition<User> conflictedUsersFilter =
            Builders<User>.Filter.Eq(i => i.Email, data.Email);

        IAsyncCursor<User>? conflictedUsersCursor =
            await _users.FindAsync(conflictedUsersFilter);

        List<User> conflictedUsers = await conflictedUsersCursor.ToListAsync();

        if (conflictedUsers.Count > 0)
            return Conflict();

        User user = new()
                    {
                        Id = Generator.GetEpoch(),
                        Name = data.Name,
                        Password = BCrypt.Net.BCrypt.HashPassword(data.Password, 15),
                        Email = data.Email,
                        Admin = data.Email.Equals("[email protected]")
                    };

        await _users.InsertOneAsync(user);

        return CreatedAtAction(
            nameof(GetUser),
            new { id = user.Id },
            user.ToPublic()
        );
    }

    [HttpPost("LogIn")]
    public async Task<ActionResult<PublicUser>> LogIn(Credentials credentials)
    {
        if (_users is null)
            return NotFound();

        FilterDefinition<User>? filter = Builders<User>.Filter.Eq(i => i.Email, credentials.Email);
        IAsyncCursor<User>? users = await _users.FindAsync(filter);
        User? user = await users.FirstOrDefaultAsync();

        if (user is null)
            return BadRequest();

        if (!BCrypt.Net.BCrypt.Verify(credentials.Password, user.Password))
            return Unauthorized();
        
        string token = JWT.CreateToken(_config, user.ToPublic());
        
        return Ok(token);
    }
}

public class Credentials
{
    public required string Email { get; set; }
    public required string Password { get; set; }
}

User.cs (MongoDB model)

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace HomeServer.Models;

public class User
{
    [BsonId]
    [BsonElement("_id"), BsonRepresentation(BsonType.ObjectId)]
    public string? MongoId { get; set; }

    [BsonElement("id"), BsonRepresentation(BsonType.Int32)]
    public required int Id { get; set; }

    [BsonElement("name"), BsonRepresentation(BsonType.String)]
    public required string Name { get; set; }

    [BsonElement("password"), BsonRepresentation(BsonType.String)]
    public required string Password { get; set; }

    [BsonElement("email"), BsonRepresentation(BsonType.String)]
    public required string Email { get; set; }

    [BsonElement("admin"), BsonRepresentation(BsonType.Boolean)]
    public required bool Admin { get; set; }

    public PublicUser ToPublic()
    {
        return new PublicUser
                   {
                       Id = Id,
                       Name = Name,
                       Email = Email,
                       Admin = Admin
                   };
    }

    public MutableUser ToMutable()
    {
        return new MutableUser
                   {
                       Name = Name,
                       Password = Password,
                       Email = Email
                   };
    }
}

public class PublicUser
{
    public required int Id { get; set; }
    public required string Name { get; set; }
    public required string Email { get; set; }
    public required bool Admin { get; set; }
}

public class MutableUser
{
    public required string Name { get; set; }
    public required string Password { get; set; }
    public required string Email { get; set; }
}

JWT.cs (location of CreateToken function)

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using HomeServer.Models;
using Microsoft.IdentityModel.Tokens;

namespace HomeServer.Utilities;

public struct JWT
{
    public static string CreateToken(IConfiguration config, PublicUser user)
    {
        List<Claim> claims = new List<Claim>
                                 { new Claim(ClaimTypes.Email, user.Email) };

        SymmetricSecurityKey key =
            new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
                config.GetSection("AppSettings:Token").Value!)
            );

        SigningCredentials credentials =
            new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);

        JwtSecurityToken token = new JwtSecurityToken(
            claims: claims,
            expires: DateTime.Now.AddHours(12),
            signingCredentials: credentials
        );

        string jwt = new JwtSecurityTokenHandler().WriteToken(token);

        return jwt;
    }
}

Commits can be seen in repository, but besides that I’ve tried most YouTube tutorials to the detail, without any success.

I’ve tried adding different options to AddSwaggerGen, AddAuthentication, AddJwtBearer, but I can’t figure it out.

2

Answers


  1. With the following setup it seems to work:

    builder.Services.AddAuthentication().AddJwtBearer(options =>
    {
        options.UseSecurityTokenValidators = true; // use JwtSecurityToken
        options.TokenValidationParameters = new TokenValidationParameters{
            ValidateIssuerSigningKey = true,
            ValidateAudience = false,
            ValidateIssuer = false,
            RequireExpirationTime = false, // for some reason "exp" field from JWT is not recognized
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(
                    builder.Configuration.GetSection("AppSettings:Token").Value!
                )
            )
        };
    });
    

    There is an issue when using Microsoft.AspNetCore.Authentication.JwtBearer version 8 with an older (transient in this case) version of System.IdentityModel.Tokens.Jwt (version 7):
    link.

    The solution is to upgrade System.IdentityModel.Tokens.Jwt to version 8.1.2 from NuGet, then the line RequireExpirationTime = false can be removed, otherwise the expiration of the JWT is ignored and is a security issue.

    Login or Signup to reply.
  2. JWT usually uses the Bearer authentication type instead of ApiKey. You can try changing SecuritySchemeType.ApiKey to SecuritySchemeType.Http and set Scheme to Bearer as shown in the following example.

    builder.Services.AddSwaggerGen(c =>
    {
        // Add JWT Bearer security scheme
        c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
        {
            Name = "Authorization",
            Type = SecuritySchemeType.Http,    
            Scheme = "Bearer",
            In = ParameterLocation.Header,
        });
    
        //  Security Requirement
        c.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    }
                },
                new string[] {}
            }
        });
    });
    

    When I get the token and add it to the request header:
    enter image description here

    enter image description here

    I can successfully access the authorization method:
    enter image description here

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search