feat(#13): S-12c-1 — behandel BFF auth + werkbak (ADR-0013) #85
@@ -120,10 +120,21 @@ internal static class BehandelAuth
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var realmAccess = principal.FindFirst("realm_access")?.Value;
|
var realmAccess = principal.FindFirst("realm_access")?.Value;
|
||||||
if (string.IsNullOrEmpty(realmAccess))
|
if (string.IsNullOrWhiteSpace(realmAccess))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var roles = JsonSerializer.Deserialize<RealmAccess>(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>(realmAccess)?.Roles ?? [];
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var role in roles)
|
foreach (var role in roles)
|
||||||
identity.AddClaim(new Claim(identity.RoleClaimType, role));
|
identity.AddClaim(new Claim(identity.RoleClaimType, role));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Hosting;
|
|||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.IdentityModel.Protocols;
|
||||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
@@ -23,9 +24,34 @@ internal sealed class BffFactory : WebApplicationFactory<Program>
|
|||||||
public FakeDomainClient Domain { get; } = new();
|
public FakeDomainClient Domain { get; } = new();
|
||||||
public FakeProjectionClient Projection { get; } = new();
|
public FakeProjectionClient Projection { get; } = new();
|
||||||
|
|
||||||
|
private static void ValidateWithTestKey(IServiceCollection services, string scheme) =>
|
||||||
|
services.Configure<JwtBearerOptions>(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<OpenIdConnectConfiguration>(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)
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
{
|
{
|
||||||
builder.UseSetting("Keycloak:Authority", "https://keycloak.invalid/realms/digid");
|
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:Domain:BaseUrl", "http://domain.invalid/");
|
||||||
builder.UseSetting("Downstream:Projection:BaseUrl", "http://projection.invalid/");
|
builder.UseSetting("Downstream:Projection:BaseUrl", "http://projection.invalid/");
|
||||||
|
|
||||||
@@ -34,23 +60,11 @@ internal sealed class BffFactory : WebApplicationFactory<Program>
|
|||||||
services.AddSingleton<IDomainClient>(Domain);
|
services.AddSingleton<IDomainClient>(Domain);
|
||||||
services.AddSingleton<IProjectionClient>(Projection);
|
services.AddSingleton<IProjectionClient>(Projection);
|
||||||
|
|
||||||
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
|
// 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
|
||||||
// Validate locally against the test key; never reach out for OIDC metadata.
|
// parameters are swapped here.
|
||||||
options.Authority = null;
|
ValidateWithTestKey(services, JwtBearerDefaults.AuthenticationScheme);
|
||||||
options.MetadataAddress = null!;
|
ValidateWithTestKey(services, "medewerker");
|
||||||
options.RequireHttpsMetadata = false;
|
|
||||||
options.Configuration = new OpenIdConnectConfiguration();
|
|
||||||
options.TokenValidationParameters = new TokenValidationParameters
|
|
||||||
{
|
|
||||||
ValidateIssuer = false,
|
|
||||||
ValidateAudience = false,
|
|
||||||
ValidateLifetime = true,
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
IssuerSigningKey = TestSigningKey,
|
|
||||||
ClockSkew = TimeSpan.Zero,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user