From 13f9e7be7f4f50a655d49f44c29ba7fed4a0d8b5 Mon Sep 17 00:00:00 2001 From: WorkClub Automation Date: Fri, 20 Mar 2026 11:01:56 +0100 Subject: [PATCH] Fix JWT validation by configuring custom signing key resolver - Added IssuerSigningKeyResolver to fetch JWKS directly from internal Keycloak URL - This bypasses the localhost:8080 URLs in Keycloak's discovery document - Ensures JWT tokens are validated against correct signing keys --- backend/WorkClub.Api/Program.cs | 50 +++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/backend/WorkClub.Api/Program.cs b/backend/WorkClub.Api/Program.cs index 0382631..132d300 100644 --- a/backend/WorkClub.Api/Program.cs +++ b/backend/WorkClub.Api/Program.cs @@ -51,21 +51,53 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) options.RequireHttpsMetadata = false; options.MapInboundClaims = false; - // For Docker internal communication, use the direct Keycloak URL for metadata - // This bypasses the hostname mismatch in Keycloak's discovery endpoint + // For Docker internal communication, configure metadata and signing key resolution + // to bypass the hostname mismatch in Keycloak's discovery endpoint var keycloakAuthority = builder.Configuration["Keycloak:Authority"]; + var keycloakInternalUrl = "http://keycloak:8081"; + if (keycloakAuthority?.Contains("keycloak:") == true) { + // Set metadata address to internal Keycloak URL options.MetadataAddress = $"{keycloakAuthority}/.well-known/openid-configuration"; + + // Configure custom signing key resolver to fetch from internal Keycloak URL + // This overrides the URLs returned in the discovery document + var httpClient = new HttpClient(); + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => + { + // Fetch JWKS from internal Keycloak URL + var jwksUrl = $"{keycloakInternalUrl}/realms/workclub/protocol/openid-connect/certs"; + try + { + var response = httpClient.GetStringAsync(jwksUrl).GetAwaiter().GetResult(); + var jwks = new Microsoft.IdentityModel.Tokens.JsonWebKeySet(response); + return jwks.Keys; + } + catch (Exception ex) + { + Console.WriteLine($"Failed to fetch JWKS from {jwksUrl}: {ex.Message}"); + return Array.Empty(); + } + } + }; } - - options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + else { - ValidateIssuer = false, // Disabled for local dev - external clients use localhost:8080, internal use keycloak:8080 - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true - }; + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true + }; + } options.Events = new JwtBearerEvents { OnAuthenticationFailed = context =>