## What & why First half of **S-12c** (behandel-portal backend), per **ADR-0013** (decisions recorded in #84): - **BFF multi-realm auth.** A second JWT bearer scheme (`medewerker`) alongside the default `digid` scheme. On validation it lifts Keycloak's `realm_access.roles` onto the principal, and a `behandelaar` policy (medewerker scheme + `behandelaar` role) gates `/behandel/*`. Self-service keeps the digid scheme. - **Werkbak = Flowable tasks.** The domain `Werkbak` query reads the open `Beoordelen` tasks (§8.2, S-12b's `IUserTaskClient`) and enriches each with its aggregate's bsn + status; `GET /behandel/werkbak` (domain) is proxied by the BFF `GET /behandel/werkbak` behind the behandelaar policy. The read projection stays the anonymous openbaar model (no premature `IN_BEHANDELING`/personal-data plumbing — deferred in ADR-0008). Behavior: `/behandel/werkbak` is **401** without a token, **403** for a medewerker lacking the role, **200 + werkbak** for a behandelaar. **S-12c-2** (next): `POST /behandel/registrations/{id}/decide` → domain decision + complete the Flowable task. ## Definition of Done - [x] Linked issue: #13 (umbrella, `refs`); closes the adr-proposal #84 - [x] Tests first; red → green per layer - [x] Unit + acceptance green (`make unit`): domain 78, bff 23, acceptance 9 (+ acl/event-subscriber unaffected) - [x] api-client `test` green; openapi.json regenerated (drift guard passes) - [x] Mutation ≥ break(90): **domain 100%, bff 100%** - [x] ADR-0013 added; `Keycloak__MedewerkerAuthority` wired into compose - [ ] CI green (pending) Part of #13. closes #84 Reviewed-on: #85
This commit was merged in pull request #85.
This commit is contained in:
@@ -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<Program>
|
||||
public FakeDomainClient Domain { 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)
|
||||
{
|
||||
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<Program>
|
||||
services.AddSingleton<IDomainClient>(Domain);
|
||||
services.AddSingleton<IProjectionClient>(Projection);
|
||||
|
||||
services.Configure<JwtBearerOptions>(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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -60,12 +74,16 @@ internal sealed class FakeDomainClient : IDomainClient
|
||||
{
|
||||
public string? SubmittedBsn { get; private set; }
|
||||
public SubmitAccepted Result { get; set; } = new("reg-123", "Ingediend");
|
||||
public List<WerkbakItem> Werkbak { get; } = [];
|
||||
|
||||
public Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default)
|
||||
{
|
||||
SubmittedBsn = bsn;
|
||||
return Task.FromResult(Result);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<WerkbakItem>> GetWerkbakAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<WerkbakItem>>(Werkbak);
|
||||
}
|
||||
|
||||
/// <summary>Serves a configurable set of projection rows.</summary>
|
||||
|
||||
Reference in New Issue
Block a user