tests+infra: OIDC system-test coverage + drop .dev from local compose files#185
Merged
Conversation
…-era duplicates
- docker-compose.dev.yml -> docker-compose.yml (root manifest + api,
frontend, stalwart, listmonk service includes). docker compose up
is now the canonical command.
- Delete the unused services/{api,frontend,listmonk}/docker-compose.yml
Swarm-era variants left over from before the k3s+Flux migration; the
active dev compose now lives at the .dev-less name.
- Update references in dev-setup.sh, scripts/generate_openapi.sh,
.github/workflows/ci.yml path filter, README.md, the live-IT
Kdocs, and the mariadb release.yaml comment.
… forward-auth Carries the OIDC test patterns proven in personal-stack into :services:system-tests as a new oidc/ package alongside the existing frontend/ Playwright suite. All run via the embedded @SpringBootTest on :8080 — no docker-compose dependency at CI time. New tests: - JwksDiscoverySystemTest: /.well-known/oauth-authorization-server advertises authorize/token/jwks; /oauth2/jwks publishes RSA signing keys with kid; discovery jwks_uri path matches the in-process endpoint. - AuthorizeRedirectSystemTest: parametrized over both registered clients (headlamp, vault). Anonymous → 302 to /login with the original /oauth2/authorize URL preserved as redirect=. Member → 403 from the downstream-client gating filter (the admin-only client guard in AuthorizationServerConfig). Admin → 302 to the configured redirect_uri with code= and state= round-tripped. - OidcTokenClaimsSystemTest: full PKCE code-grant for headlamp. Exchanges code at /oauth2/token, decodes both ID + access tokens, asserts OidcTokenCustomizer's claim shape end-to-end (sub, aud, roles, username, email; ID-token groups maps ADMIN → k8s-admin + member). Vault grant omitted — its CLIENT_SECRET_BASIC config is empty in the test profile and out of scope here; vault authorize is still covered above. - ForwardAuthChainSystemTest: lifts ForwardAuthControllerIT to the system-test layer over real HTTP. Parametrized across every host in HOST_ROLE (vault, headlamp, traefik = ADMIN; listmonk, stalwart = BOARD). Asserts anonymous → /login, member → 302 /unauthorized?service=, admin → 200 + X-User-Id/X-User-Groups, board pass-through on board-gated hosts and 302 on admin-gated hosts, plus the unknown-host ADMIN fallback. - OidcAuthorizePlaywrightTest: drives /oauth2/authorize through Playwright's APIRequest (matching personal-stack's RabbitMqOidcPlaywrightTest pattern). Parametrized over both clients. APIRequest rather than Page navigation because the configured redirect_uri is a production hostname — APIRequest stops at the 302, Page would resolve DNS and hang. Plus OidcSystemTestBase (slim @SpringBootTest base with a no-follow HttpClient and bearer-token helpers) and OidcTestHelper (PKCE S256 + JWT payload decode + form-encode + queryParam).
…e principal Under Spring Security 6's requireExplicitSave=true default, mutating SecurityContextHolder isn't enough — downstream filters that resolve the principal through the deferred supplier (OAuth2 SAS authorize, for one) read the SecurityContextRepository, not the holder, and would otherwise see an anonymous context. Symptom: bearer-authenticated /oauth2/authorize returned 401 over real HTTP. MockMvc never tripped it because it skips the deferred-supplier round trip; ForwardAuthControllerIT consequently passed while every new OIDC system test failed (run 25655557367, shards 2 + 4). The fix is two beans: - JwtAuthFilter now takes a SecurityContextRepository and calls saveContext after authenticating, so the in-flight ThreadLocal is also written through to the per-request attribute the deferred supplier reads from. - A new SecurityContextRepositoryConfig publishes the repository (DelegatingSecurityContextRepository wrapping RequestAttributeSecurityContextRepository + HttpSession one — the Spring Security 6 default shape) and a FilterRegistrationBean that disables Spring Boot's auto-registration of @component JwtAuthFilter as a raw servlet filter outside the security chain. The config lives outside SecurityConfig.kt to avoid a circular dependency: SecurityConfig already takes JwtAuthFilter via the constructor, so it can't also be the producer of a bean JwtAuthFilter depends on. Local: :services:api:integrationTest --tests "*ForwardAuthControllerIT*" green (7/7). System tests deferred to CI — host :8080 is busy with an unrelated container on this machine.
… Bearer
The actual fix for the 401s on /oauth2/authorize. Root cause was
.oidc(Customizer.withDefaults()) in AuthorizationServerConfig, which
auto-enables an OidcUserInfoEndpointConfigurer. That implicitly wires
oauth2ResourceServer.jwt() onto the SAS chain, which both:
- adds a BearerTokenAuthenticationFilter that intercepts every
Authorization: Bearer request to /oauth2/* and validates the JWT
against the SAS RSA JWKS — our HS256 session tokens fail, and the
filter responds 401 directly via its own BearerTokenAuthentication-
EntryPoint.
- registers that same 401 entry point as the default for any request
matching Accept: */*, X-Requested-With, or REST content types —
shadowing our loginRedirectEntryPoint for anonymous requests.
Production isn't affected: real browsers send the BSH_AUTH cookie (not
Authorization: Bearer) and Accept: text/html with a quality on */*, so
neither shortcut fires. Tests just need to mirror that.
OidcSystemTestBase.get now sends `Cookie: BSH_AUTH=<jwt>` (matching
AuthTokenCookieService's default cookie name, overridable via
security.auth-cookie.name) and Accept: text/html on every request.
sessionTokenFor replaces bearerToken; the parameter is renamed
sessionToken everywhere so the call sites are obvious. The Playwright
authorize test does the same via RequestOptions headers.
The token-exchange POST in OidcTokenClaimsSystemTest is unchanged: it
hits /oauth2/token without an Authorization header (PKCE), which never
trips the bearer auth filter.
Two remaining test-side issues after the cookie-auth fix landed:
- Jackson 3.1 introduced `JsonNode.map(Function)` as an Optional-style
(apply-to-whole-node) operator. It shadows Kotlin's `Iterable.map`
when chained on a JsonNode, so `arrayNode.map { it.asString() }` ran
the lambda on the entire ArrayNode rather than its elements and blew
up with a coercion error. Two new OidcTestHelper helpers iterate via
the explicit `iterator().asSequence()` path; tests use those.
- Tomcat resolves the relative `/login?redirect=...` path returned by
loginRedirectEntryPoint to an absolute URL before writing the
Location header, so `assertThat(location).startsWith("/login?...")`
failed even though the redirect itself was correct. Loosened to
`contains(...)` on the same anchor.
JWT `aud` is serialized as a bare string when there's a single audience (Nimbus + RFC 7519 default). The array-only iteration returned an empty list, breaking the access-token aud assertion in OidcTokenClaimsSystemTest. Branch on isArray and fall through to the text-node case so the helper works for both shapes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related changes shipped together since they touched the same set of files:
1. Drop
.devfrom local docker-compose files (6486eecc)docker compose upis now the canonical command.docker-compose.dev.ymland the four service-leveldocker-compose.dev.ymlfiles lose the.devsuffix. The Swarm-eraservices/{api,frontend,listmonk}/docker-compose.ymlvariants (left over from before the k3s+Flux migration in #117) are removed — they aren't referenced by any current script, runbook, or CI workflow, and they would otherwise collide with the renamed files.References updated:
dev-setup.sh,scripts/generate_openapi.sh.github/workflows/ci.ymlpath filterREADME.mddevelopment-setup sectionBrevoContactAdapterLiveITandListmonkContactAdapterLiveITplatform/cluster/flux/apps/data/mariadb/release.yaml2. OIDC/SSO system-test coverage (
fcb742f9)Carries the OIDC test patterns proven in
personal-stackinto:services:system-testsas a newoidc/package alongside the existingfrontend/Playwright suite. All run via the embedded@SpringBootTeston:8080— no docker-compose dependency at CI time.JwksDiscoverySystemTest/.well-known/oauth-authorization-server+/oauth2/jwkscontractAuthorizeRedirectSystemTest/oauth2/authorizefor both headlamp + vault: anon → /login, member → 403 (admin-only client gate), admin → 302 withcode=OidcTokenClaimsSystemTestOidcTokenCustomizerclaim shape (sub, aud, roles, username, email; groups → k8s-admin + member)ForwardAuthChainSystemTest/oauth2/forward-authparametrized across every host inHOST_ROLE; liftsForwardAuthControllerITfrom integration → system layerOidcAuthorizePlaywrightTestPlus
OidcSystemTestBase(slim@SpringBootTestbase with a no-followHttpClient+ bearer-token helpers) andOidcTestHelper(PKCE S256, JWT payload decode, form-encode, query-param parse).Test plan
tests-and-compose-rename— all 4 system-test shards + api unit + integrationdocker compose upboots the local stack with no-fflagbash dev-setup.shstill produces working env files + TLS certsbash scripts/generate_openapi.shregenerates the spec against the renamed compose./gradlew :services:system-tests:test --tests "net.blueshell.api.system.oidc.*"— focused run on the new suite