Search code examples
c#authentication.net-corejwtasp.net-core-webapi

Why doesn't JWT authentication work in ASP.NET Core Web API?


Basically I use an ASP.NET Core Web API, and the controller returns Ok when I send a request to Login, it sets the refresh and access tokens as http only cookies well.

But there's a problem: the user isn't authenticated (I mean that even if I try to go to a [Authorize] route it won't work, or trying to access User... with identity, still doesn't work) for some reason. I checked manually with JWT and the exp date, aud and issue are well set, I simply don't know what's the problem.

In the program.cs new JwtBearerEvents doesn't even print anything to the console. Please help me.

This is a part of the service for the controller:

public async Task<bool> Login(LoginUserModel user)
{
    var identityUser = await _userManager.FindByNameAsync(user.UserName!);

    if (identityUser is null)
    {
        return false;
    }

    return await _userManager.CheckPasswordAsync(identityUser, user.Password!);
}

public string GenerateTokenString(LoginUserModel user)
{
    var tokenId = Guid.NewGuid().ToString(); // Unique identifier for each token
 
    var claims = new List<Claim>
                     {
                         new Claim(ClaimTypes.NameIdentifier, user.UserName !),
                         new Claim(ClaimTypes.Role, "Admin"), // This should be dynamic based on actual user role
                         new Claim("TokenId", tokenId) // Include the unique TokenId in the token
                     };

    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
    var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha512Signature);

    var tokenDescriptor = new SecurityTokenDescriptor
                              {
                                  Subject = new ClaimsIdentity(claims),
                                  Expires = DateTime.UtcNow.AddMinutes(60), // Token validity period
                                  Issuer = _config["Jwt:Issuer"],
                                  Audience = _config["Jwt:Audience"],
                                  SigningCredentials = signingCredentials
                              };

    var tokenHandler = new JwtSecurityTokenHandler();
    var token = tokenHandler.CreateToken(tokenDescriptor);

    return tokenHandler.WriteToken(token);
}

public async Task<string> GenerateRefreshToken(string userName)
{
   var user = await _userManager.FindByNameAsync(userName);

   if (user == null) 
       return null!;

    // Create a new refresh token
    var refreshToken = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
    user.RefreshToken = refreshToken;
    user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(14); // Set refresh token validity

    await _userManager.UpdateAsync(user);

    return refreshToken;
}

This is the program.cs:

using EasyLink.Server.Database.Context;
using EasyLink.Server.Identity;
using EasyLink.Server.Services.Auth;
using EasyLink.Server.Services.Categories;
using EasyLink.Server.Services.Stripe;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Stripe;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient();

// Configure DbContext.
builder.Services.AddDbContext<AuthDbContext>(options =>
   options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Configure Identity.
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
   options.Password.RequiredLength = 8;
   options.Password.RequireLowercase = true;
   options.Password.RequireUppercase = true;
   options.Password.RequireDigit = true;
   options.Password.RequireNonAlphanumeric = false;
}).AddEntityFrameworkStores<AuthDbContext>()
 .AddDefaultTokenProviders();

// Configure JWT Authentication.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
   .AddJwtBearer(options =>
   {
       options.TokenValidationParameters = new TokenValidationParameters
       {
           ValidateIssuer = true,
           ValidateAudience = true,
           ValidateLifetime = true,
           ValidateIssuerSigningKey = true,
           ValidIssuer = builder.Configuration["Jwt:Issuer"],
           ValidAudience = builder.Configuration["Jwt:Audience"],
           IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
           ClockSkew = TimeSpan.Zero
       };
       options.Events = new JwtBearerEvents
       {
           OnAuthenticationFailed = context =>
           {
               Console.WriteLine("\n\n\n\n\n\n\nAuthentication failed: " + context.Exception.Message);
               return Task.CompletedTask;
           },
           OnTokenValidated = context =>
           {
               Console.WriteLine("\n\n\n\n\n\n\nToken validated successfully.");
               return Task.CompletedTask;
           },
           OnMessageReceived = context =>
           {
               Console.WriteLine("\n\n\n\nerror\n\n\n");
               if (context.Request.Cookies.ContainsKey("AccessToken"))
               {
                   context.Token = context.Request.Cookies["AccessToken"];
               }
               return Task.CompletedTask;
           }
       };
   });

// Additional services.
builder.Services.AddTransient<IAuthService, AuthService>();
builder.Services.AddScoped<IStripeService, StripeService>();
builder.Services.AddHttpClient("CategoryApiClient", client =>
{
   client.BaseAddress = new Uri("https://www.autovit.ro/api/");
   client.DefaultRequestHeaders.Add("Accept", "application/json");
});
builder.Services.AddScoped<ICategoriesService, CategoriesService>();
builder.Services.AddCors(options =>
{
   options.AddPolicy("AllowSpecificOrigin", builder =>
   {
       builder.WithOrigins("https://localhost:5173")
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials();
   });
});
StripeConfiguration.ApiKey = builder.Configuration["Stripe:Key"];

var app = builder.Build();

// Middleware pipeline configuration.
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHsts();
app.UseCors("AllowSpecificOrigin");

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

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.MapFallbackToFile("/index.html");
app.Run();

This is a part of the authcontroller:

[HttpPost("Login")]
public async Task<IActionResult> Login([FromBody] LoginUserModel user)
{
   if (!ModelState.IsValid)
   {
       return BadRequest();
   }

   if (!await _authService.CheckPayment(user.UserName!))
   {
       return Unauthorized("Payment required.");
   }

   if (await _authService.Login(user))
   {
       var tokenString = _authService.GenerateTokenString(user);
       var refreshToken = await _authService.GenerateRefreshToken(user.UserName!);

       // Store access token and refresh token in HttpOnly cookies
       var cookieOptions = new CookieOptions
       {
           HttpOnly = true,
           Secure = true,
           SameSite = SameSiteMode.Strict,
           Expires = DateTime.UtcNow.AddMinutes(60)
       };

       Response.Cookies.Append("AccessToken", tokenString, cookieOptions);

       // Extend the cookie's expiry for refresh token since it should be valid longer than access token
       var refreshCookieOptions = new CookieOptions
       {
           HttpOnly = true,
           Secure = true,
           SameSite = SameSiteMode.Strict,
           Expires = DateTime.UtcNow.AddDays(14) // Refresh token validity
       };

       Response.Cookies.Append("RefreshToken", refreshToken, refreshCookieOptions);

       return Ok(new { message = "Login successful", accessToken = tokenString, refreshToken = refreshToken });
   }

   return Forbid("Invalid credentials.");
}

[HttpGet("verify")]
public IActionResult Verify()
{
    var isAuthenticated = User.Identity!.IsAuthenticated;

    return Ok(new { isAuthenticated });
}

I'm trying to Login a user...

Basically the Login returns ok but it still doesn't authenticate, I tried on [Authorize] routes, and tries User.Identity!.IsAuthenticated; and still returns false, the user is stored in the database well too..


Solution

  • Ok, so I found the problem myself, the problem was that I didn't set well the scheme, that's what you should modify:

    builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
     ...
    });
    app.Use(async (context, next) =>
    {
        var token = context.Request.Cookies["AccessToken"];
        if (!string.IsNullOrEmpty(token))
        {
            context.Request.Headers.Append("Authorization", $"Bearer {token}");
            Console.WriteLine("Token appended to header: " + token);
        }
        else
        {
            Console.WriteLine("No token found in cookies");
        }
        await next();
    });
    
    

    fun fact, I found the problem when I fetched a wrong password for a valid email on the frontend :)))