From bab3453e41bc67d8b4fa2df87f30977896269dff Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Tue, 2 Jun 2026 11:18:10 +0330 Subject: [PATCH] fix(auth): read role claim under mapped name so Owner/Manager gates work ROOT CAUSE of demo-seed/billing/etc. returning 403 for real owners: .NET's JWT handler remaps the short "role" claim to ClaimTypes.Role on inbound, so TenantMiddleware's FindFirst("role") returned null and tenant.Role (EmployeeRole?) stayed null. EnsureManager/EnsureOwner then rejected even a valid Owner token with MANAGER_REQUIRED / OWNER_REQUIRED, while reads (no role gate) worked and [Authorize(Roles=...)] worked (it reads the remapped claim). Now reads the role under both MeeziClaimTypes.Role ("role") and ClaimTypes.Role. Same fix applied to the AuthController whoami role. Fixes demo seed, subscription billing, and every other tenant.Role-gated action. Co-Authored-By: Claude Opus 4.8 --- src/Meezi.API/Controllers/AuthController.cs | 5 ++++- src/Meezi.API/Middleware/TenantMiddleware.cs | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Meezi.API/Controllers/AuthController.cs b/src/Meezi.API/Controllers/AuthController.cs index 9d9dd00..002aeef 100644 --- a/src/Meezi.API/Controllers/AuthController.cs +++ b/src/Meezi.API/Controllers/AuthController.cs @@ -198,7 +198,10 @@ public class AuthController : ControllerBase ExpiresAt: expiresAt, UserId: userId, CafeId: User.FindFirstValue(MeeziClaimTypes.CafeId) ?? string.Empty, - Role: User.FindFirstValue(MeeziClaimTypes.Role) ?? string.Empty, + // .NET remaps the short "role" claim to ClaimTypes.Role on inbound; read both. + Role: User.FindFirstValue(MeeziClaimTypes.Role) + ?? User.FindFirstValue(System.Security.Claims.ClaimTypes.Role) + ?? string.Empty, PlanTier: User.FindFirstValue(MeeziClaimTypes.PlanTier) ?? string.Empty, Language: User.FindFirstValue(MeeziClaimTypes.Language) ?? string.Empty, Actor: User.FindFirstValue(MeeziClaimTypes.Actor) ?? MeeziActorKinds.Merchant, diff --git a/src/Meezi.API/Middleware/TenantMiddleware.cs b/src/Meezi.API/Middleware/TenantMiddleware.cs index 5dcd636..8c28526 100644 --- a/src/Meezi.API/Middleware/TenantMiddleware.cs +++ b/src/Meezi.API/Middleware/TenantMiddleware.cs @@ -92,7 +92,12 @@ public class TenantMiddleware { scopedMerchant.CafeId = cafeId; - var roleClaim = context.User.FindFirst(MeeziClaimTypes.Role)?.Value; + // .NET's JWT handler remaps the short "role" claim to ClaimTypes.Role + // on inbound, so FindFirst("role") returns null and tenant.Role would + // stay null — making EnsureManager/EnsureOwner reject even a real owner. + // Read both the raw claim and the mapped one. + var roleClaim = context.User.FindFirst(MeeziClaimTypes.Role)?.Value + ?? context.User.FindFirst(System.Security.Claims.ClaimTypes.Role)?.Value; if (Enum.TryParse(roleClaim, ignoreCase: true, out var role)) scopedMerchant.Role = role;