WIP
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -6,6 +8,7 @@ using Microsoft.IdentityModel.Tokens;
|
||||
using ROLAC.API.Data;
|
||||
using ROLAC.API.Data.Interceptors;
|
||||
using ROLAC.API.Entities;
|
||||
using ROLAC.API.Json;
|
||||
using ROLAC.API.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -47,6 +50,11 @@ builder.Services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(opt =>
|
||||
{
|
||||
// Keep JWT claim names exactly as written ("role", "sub", "email").
|
||||
// Without this, .NET 8's JsonWebTokenHandler may remap "role" to the
|
||||
// long ClaimTypes.Role URI, which conflicts with RoleClaimType = "role".
|
||||
opt.MapInboundClaims = false;
|
||||
|
||||
opt.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
@@ -56,9 +64,37 @@ builder.Services
|
||||
ValidIssuer = config["Jwt:Issuer"],
|
||||
ValidAudience = config["Jwt:Audience"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
|
||||
// Roles were written as JWT short name "role"; map to ClaimTypes.Role for [Authorize].
|
||||
NameClaimType = "sub",
|
||||
RoleClaimType = "role",
|
||||
ClockSkew = TimeSpan.Zero,
|
||||
ClockSkew = TimeSpan.FromMinutes(1),
|
||||
};
|
||||
|
||||
// Diagnostic events — visible in the API console while debugging 401s.
|
||||
opt.Events = new JwtBearerEvents
|
||||
{
|
||||
OnAuthenticationFailed = ctx =>
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[JWT] Auth failed: {ctx.Exception.GetType().Name} — {ctx.Exception.Message}");
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnChallenge = ctx =>
|
||||
{
|
||||
// Fires when a 401 challenge is about to be sent.
|
||||
Console.WriteLine(
|
||||
$"[JWT] Challenge: error={ctx.Error}, description={ctx.ErrorDescription}");
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnForbidden = ctx =>
|
||||
{
|
||||
// Fires when user IS authenticated but lacks the required role (403).
|
||||
Console.WriteLine(
|
||||
$"[JWT] Forbidden: user={ctx.HttpContext.User.Identity?.Name}, " +
|
||||
$"roles=[{string.Join(',', ctx.HttpContext.User.Claims
|
||||
.Where(c => c.Type == "role")
|
||||
.Select(c => c.Value))}]");
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -86,7 +122,16 @@ builder.Services.AddScoped<IUserManagementService, UserManagementService>();
|
||||
// ---------------------------------------------------------------------------
|
||||
// Swagger / MVC
|
||||
// ---------------------------------------------------------------------------
|
||||
builder.Services.AddControllers();
|
||||
builder.Services
|
||||
.AddControllers()
|
||||
.AddJsonOptions(opt =>
|
||||
{
|
||||
// camelCase in/out + tolerant DateOnly (accepts "yyyy-MM-dd" or full ISO datetime).
|
||||
opt.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
opt.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
|
||||
opt.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
|
||||
opt.JsonSerializerOptions.Converters.Add(new TolerantDateOnlyConverter());
|
||||
});
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(opt =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user