api: stamp kid/alg/use on Transit JWKS + Playwright e2e harness#191
Merged
Conversation
Reproduces the production /oauth2/jwks 500 locally and pins the fix under a real Vault. Verified red on bug-state, green with fix. Pieces: - `services/vault/docker-compose.yml` (oidc-e2e profile) — Vault dev container + one-shot init that enables transit and creates an RSA api-jwt key. Pulled into root compose via include. - `docker-compose.oidc-e2e.yml` — overlay that flips api into transit mode (AUTH_TRANSIT_ENABLED=true, VAULT_ADDR=..., SPRING_CLOUD_VAULT_TOKEN=root) and drops the listmonk-setup precondition so the api can boot without Listmonk for OIDC e2e. - `services/system-tests/.../VaultTransitJwksPlaywrightTest.kt` — drives /oauth2/jwks against the live api via Playwright's native APIRequest (not chromium — chromium's network stack trips ECONNRESET on plain-HTTP localhost). @tag("vault-oidc-live"). Includes a /health readiness wait in @BeforeAll because Spring Boot devtools may be mid-restart after any preceding gradle build touches the shared /src mount. - `vaultOidcLiveTest` gradle task with `useJUnitPlatform.includeTags` scoped to the new tag (overrides the project-wide system tag filter). Two collateral fixes the test surfaced: - `Dockerfile-dev`: pin base to `eclipse-temurin:21-jdk-noble`. The un-pinned `21-jdk` tag is now ubuntu26.04, which Playwright 1.58 can't install browsers for on arm64 — `installChromium` failed. - `VaultTransitJwkSource`: catch Throwable, not Exception, in init/refresh. The bcpkix-missing failure is a `NoClassDefFoundError` (Error subclass), so the previous catch let it abort bean creation instead of degrading to an empty JWKSet. Test updated to use NoClassDefFoundError, matching the actual prod failure mode. To repro locally: docker compose -f docker-compose.yml -f docker-compose.oidc-e2e.yml \ --profile oidc-e2e up -d ./gradlew :services:system-tests:vaultOidcLiveTest
4 tasks
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 issues, related:
1. Bug — root cause of the post-#190 Vault sign-in failure.
After #190 fixed the JWKS 500, Vault sign-in then died with:
Diagnosed by signing a known payload via
transit/sign/api-jwtand verifying directly with the JWKS-published public key — the crypto is fine (RSA modulus matches, RS256 verifies, Vault's algorithm config is right). So the failure was upstream of the actual signature check.The published JWK has no
kid, noalg, nouse— just{kty, e, n}. ButVaultTransitJwtEncoderwriteskid="api-jwt:v1"into the JWT header. Vault's verifier is coreos/go-oidc-v3, which filters JWKS keys bykid:Every key gets skipped → "invalid signature". Fix: rebuild each published JWK with
kid="$keyName:v$version"matching the encoder, plusalg=RS256anduse=sig.2. Test infra — Playwright OIDC e2e harness.
Vault dev container + transit init under the
oidc-e2ecompose profile;vaultOidcLiveTestgradle task running a Playwright system test that drives/oauth2/jwksagainst the live api over a real Vault. The test was the one that exposed the kid bug end-to-end — locally reproduced before fixing.Includes one collateral fix the local repro forced:
Dockerfile-devpinned toeclipse-temurin:21-jdk-noble(the un-pinned21-jdkresolves to ubuntu26.04, which Playwright 1.58 can't install browsers for on arm64).Also tightens
VaultTransitJwkSourcetocatch Throwable(wasException): the bcpkix-missing failure that #190 fixes is aNoClassDefFoundError, anErrorsubclass — the narrow catch let it abort bean creation instead of degrading.Verified locally
Diagnostic script (deleted from repo) confirmed: in prod, JWKS public key === Transit v1 public key (same RSA modulus sha256), signing-then-verify round-trips successfully — proving the failure is in JWK metadata, not crypto.
After the fix, /oauth2/jwks in the local stack now publishes:
{"kty":"RSA","e":"AQAB","use":"sig","kid":"api-jwt:v1","alg":"RS256","n":"..."}How to run locally
docker compose -f docker-compose.yml -f docker-compose.oidc-e2e.yml \ --profile oidc-e2e up -d curl http://localhost:8080/oauth2/jwks # 200 with kid/alg/use ./gradlew :services:system-tests:vaultOidcLiveTestTest plan
kid="api-jwt:v1",alg="RS256",use="sig"on the published JWK/oauth2/jwksagainst the live api in composevault login -method=oidc role=…completes