Skip to content

api: stamp kid/alg/use on Transit JWKS + Playwright e2e harness#191

Merged
ExtraToast merged 1 commit into
mainfrom
platform/jwks-e2e-test
May 11, 2026
Merged

api: stamp kid/alg/use on Transit JWKS + Playwright e2e harness#191
ExtraToast merged 1 commit into
mainfrom
platform/jwks-e2e-test

Conversation

@ExtraToast

@ExtraToast ExtraToast commented May 11, 2026

Copy link
Copy Markdown
Contributor

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:

Provider.VerifyIDToken: invalid id_token: failed to verify signature:
failed to verify id token signature: invalid signature

Diagnosed by signing a known payload via transit/sign/api-jwt and 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, no alg, no use — just {kty, e, n}. But VaultTransitJwtEncoder writes kid="api-jwt:v1" into the JWT header. Vault's verifier is coreos/go-oidc-v3, which filters JWKS keys by kid:

for _, key := range keys {
    if keyID == "" || key.KeyID == keyID {        // header.kid != JWK.KeyID=""
        if payload, err := jws.Verify(&key); err == nil { return payload, nil }
    }
}
return nil, errors.New("failed to verify id token signature")

Every key gets skipped → "invalid signature". Fix: rebuild each published JWK with kid="$keyName:v$version" matching the encoder, plus alg=RS256 and use=sig.

2. Test infra — Playwright OIDC e2e harness.

Vault dev container + transit init under the oidc-e2e compose profile; vaultOidcLiveTest gradle task running a Playwright system test that drives /oauth2/jwks against 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-dev pinned to eclipse-temurin:21-jdk-noble (the un-pinned 21-jdk resolves to ubuntu26.04, which Playwright 1.58 can't install browsers for on arm64).

Also tightens VaultTransitJwkSource to catch Throwable (was Exception): the bcpkix-missing failure that #190 fixes is a NoClassDefFoundError, an Error subclass — 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":"..."}
./gradlew :services:api:test --tests VaultTransitJwkSourceTest    # 4/4
./gradlew :services:system-tests:vaultOidcLiveTest                 # SUCCESS

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:vaultOidcLiveTest

Test plan

  • Unit tests assert kid="api-jwt:v1", alg="RS256", use="sig" on the published JWK
  • Playwright e2e asserts the same on /oauth2/jwks against the live api in compose
  • Direct signing-and-verifying round-trip via cryptography lib confirms key + alg are correct end-to-end
  • After merge + deploy: vault login -method=oidc role=… completes
  • Headlamp OIDC sign-in completes

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
@ExtraToast ExtraToast added the enhancement New feature or request label May 11, 2026
@ExtraToast ExtraToast self-assigned this May 11, 2026
@ExtraToast ExtraToast merged commit d40fd4d into main May 11, 2026
14 checks passed
@ExtraToast ExtraToast changed the title tests+infra: Vault-Transit JWKS Playwright e2e + harden cache against Errors api: stamp kid/alg/use on Transit JWKS + Playwright e2e harness May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant