diff --git a/services/bff/Bff.Api/Program.cs b/services/bff/Bff.Api/Program.cs index 9823a29..871ae31 100644 --- a/services/bff/Bff.Api/Program.cs +++ b/services/bff/Bff.Api/Program.cs @@ -120,10 +120,21 @@ internal static class BehandelAuth return; var realmAccess = principal.FindFirst("realm_access")?.Value; - if (string.IsNullOrEmpty(realmAccess)) + if (string.IsNullOrWhiteSpace(realmAccess)) return; - var roles = JsonSerializer.Deserialize(realmAccess)?.Roles ?? []; + // A malformed realm_access claim must not fail authentication (a throw here becomes a 401); + // it simply yields no roles, so the authorization policy answers 403. + string[] roles; + try + { + roles = JsonSerializer.Deserialize(realmAccess)?.Roles ?? []; + } + catch (JsonException) + { + return; + } + foreach (var role in roles) identity.AddClaim(new Claim(identity.RoleClaimType, role)); } diff --git a/services/bff/Bff.Tests/BffFactory.cs b/services/bff/Bff.Tests/BffFactory.cs index e6a487a..5e74fb9 100644 --- a/services/bff/Bff.Tests/BffFactory.cs +++ b/services/bff/Bff.Tests/BffFactory.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,9 +24,34 @@ internal sealed class BffFactory : WebApplicationFactory public FakeDomainClient Domain { get; } = new(); public FakeProjectionClient Projection { get; } = new(); + private static void ValidateWithTestKey(IServiceCollection services, string scheme) => + services.Configure(scheme, options => + { + // Validate locally against the test key and NEVER reach out for OIDC metadata. A static + // configuration manager guarantees this regardless of Configure/PostConfigure ordering — + // clearing Authority alone left the medewerker scheme fetching metadata under CI timing + // (2s hang → 401), because JwtBearer's PostConfigure could still build a ConfigurationManager. + options.Authority = null; + options.MetadataAddress = null!; + options.RequireHttpsMetadata = false; + options.Configuration = new OpenIdConnectConfiguration(); + options.ConfigurationManager = + new StaticConfigurationManager(new OpenIdConnectConfiguration()); + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = TestSigningKey, + ClockSkew = TimeSpan.Zero, + }; + }); + protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseSetting("Keycloak:Authority", "https://keycloak.invalid/realms/digid"); + builder.UseSetting("Keycloak:MedewerkerAuthority", "https://keycloak.invalid/realms/medewerker"); builder.UseSetting("Downstream:Domain:BaseUrl", "http://domain.invalid/"); builder.UseSetting("Downstream:Projection:BaseUrl", "http://projection.invalid/"); @@ -34,23 +60,11 @@ internal sealed class BffFactory : WebApplicationFactory services.AddSingleton(Domain); services.AddSingleton(Projection); - services.Configure(JwtBearerDefaults.AuthenticationScheme, options => - { - // Validate locally against the test key; never reach out for OIDC metadata. - options.Authority = null; - options.MetadataAddress = null!; - options.RequireHttpsMetadata = false; - options.Configuration = new OpenIdConnectConfiguration(); - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - IssuerSigningKey = TestSigningKey, - ClockSkew = TimeSpan.Zero, - }; - }); + // Both realms validate locally against the test key (no live Keycloak). The medewerker + // scheme keeps its OnTokenValidated role-lifting from Program.cs — only the validation + // parameters are swapped here. + ValidateWithTestKey(services, JwtBearerDefaults.AuthenticationScheme); + ValidateWithTestKey(services, "medewerker"); }); } }