From bb373a6b8e810c758922e37bad49af4019ef44dc Mon Sep 17 00:00:00 2001 From: WorkClub Automation Date: Thu, 19 Mar 2026 22:13:40 +0100 Subject: [PATCH] Fix admin authorization check - properly parse realm_access claim The realm_access claim in JWT is a JSON object, not a simple string. Previous string contains check was looking for escaped quotes in wrong format. - Parse realm_access as JSON to extract roles array - Check if 'admin' exists in roles array - Fallback to string contains check if JSON parsing fails - Applied fix in RequireGlobalAdmin policy, TenantValidationMiddleware, and ClubRoleClaimsTransformation Fixes: Admin users getting 401 when trying to create clubs --- .../Auth/ClubRoleClaimsTransformation.cs | 33 ++++++++++++++--- .../Middleware/TenantValidationMiddleware.cs | 37 +++++++++++++++---- backend/WorkClub.Api/Program.cs | 24 +++++++++++- 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs b/backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs index ca88bcd..84d2cce 100644 --- a/backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs +++ b/backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs @@ -54,13 +54,36 @@ public class ClubRoleClaimsTransformation : IClaimsTransformation return Task.FromResult(principal); } - // --- NEW: Skip DB role lookup if user is a global admin --- - var realmAccess = principal.FindFirst("realm_access")?.Value; - if (!string.IsNullOrEmpty(realmAccess) && realmAccess.Contains("\"admin\"", StringComparison.OrdinalIgnoreCase)) + // --- NEW: Skip DB role lookup if user is a global admin --- + var realmAccess = principal.FindFirst("realm_access")?.Value; + if (!string.IsNullOrEmpty(realmAccess) && IsAdminUser(realmAccess)) + { + return Task.FromResult(principal); + } + // --------------------------------------------------------- + + static bool IsAdminUser(string realmAccess) + { + try + { + using var doc = System.Text.Json.JsonDocument.Parse(realmAccess); + if (doc.RootElement.TryGetProperty("roles", out var rolesElement) && + rolesElement.ValueKind == System.Text.Json.JsonValueKind.Array) { - return Task.FromResult(principal); + foreach (var role in rolesElement.EnumerateArray()) + { + if (role.GetString()?.Equals("admin", StringComparison.OrdinalIgnoreCase) == true) + return true; + } } - // --------------------------------------------------------- + } + catch + { + // If JSON parsing fails, fallback to string contains check + return realmAccess.Contains("admin", StringComparison.OrdinalIgnoreCase); + } + return false; + } // Look up the user's role in the database for the requested tenant _httpContextAccessor.HttpContext!.Items["TenantId"] = tenantId; diff --git a/backend/WorkClub.Api/Middleware/TenantValidationMiddleware.cs b/backend/WorkClub.Api/Middleware/TenantValidationMiddleware.cs index 0371586..0740b22 100644 --- a/backend/WorkClub.Api/Middleware/TenantValidationMiddleware.cs +++ b/backend/WorkClub.Api/Middleware/TenantValidationMiddleware.cs @@ -44,13 +44,36 @@ public class TenantValidationMiddleware if (string.IsNullOrEmpty(clubsClaim)) { - // NEW: Skip check if user is a global admin - var realmAccess = context.User.FindFirst("realm_access")?.Value; - if (!string.IsNullOrEmpty(realmAccess) && realmAccess.Contains("\"admin\"", StringComparison.OrdinalIgnoreCase)) - { - await _next(context); - return; - } + // NEW: Skip check if user is a global admin + var realmAccess = context.User.FindFirst("realm_access")?.Value; + if (!string.IsNullOrEmpty(realmAccess) && IsAdminUser(realmAccess)) + { + await _next(context); + return; + } + + static bool IsAdminUser(string realmAccess) + { + try + { + using var doc = System.Text.Json.JsonDocument.Parse(realmAccess); + if (doc.RootElement.TryGetProperty("roles", out var rolesElement) && + rolesElement.ValueKind == System.Text.Json.JsonValueKind.Array) + { + foreach (var role in rolesElement.EnumerateArray()) + { + if (role.GetString()?.Equals("admin", StringComparison.OrdinalIgnoreCase) == true) + return true; + } + } + } + catch + { + // If JSON parsing fails, fallback to string contains check + return realmAccess.Contains("admin", StringComparison.OrdinalIgnoreCase); + } + return false; + } context.Response.StatusCode = StatusCodes.Status403Forbidden; await context.Response.WriteAsJsonAsync(new { error = "User does not have clubs claim" }); diff --git a/backend/WorkClub.Api/Program.cs b/backend/WorkClub.Api/Program.cs index 9e85016..4a686e9 100644 --- a/backend/WorkClub.Api/Program.cs +++ b/backend/WorkClub.Api/Program.cs @@ -53,7 +53,29 @@ builder.Services.AddAuthorizationBuilder() .AddPolicy("RequireGlobalAdmin", policy => policy.RequireAssertion(context => { var realmAccess = context.User.FindFirst("realm_access")?.Value; - return realmAccess != null && realmAccess.Contains("\"admin\""); + if (string.IsNullOrEmpty(realmAccess)) + return false; + + try + { + using var doc = System.Text.Json.JsonDocument.Parse(realmAccess); + if (doc.RootElement.TryGetProperty("roles", out var rolesElement) && + rolesElement.ValueKind == System.Text.Json.JsonValueKind.Array) + { + foreach (var role in rolesElement.EnumerateArray()) + { + if (role.GetString() == "admin") + return true; + } + } + } + catch + { + // If JSON parsing fails, fallback to string contains check + return realmAccess.Contains("admin", StringComparison.OrdinalIgnoreCase); + } + + return false; })) .AddPolicy("RequireManager", policy => policy.RequireRole("Manager")) .AddPolicy("RequireMember", policy => policy.RequireRole("Manager", "Member"))