Skip to content

fix(workloadapi): handle rejected JWT bundle updates in JWT sources - #482

Merged
maxlambrecht merged 2 commits into
spiffe:mainfrom
NopeCharon:fix/jwt-bundle-parse-unsupported-key
Oct 29, 2026
Merged

maxlambrecht merged 2 commits into
spiffe:mainfrom
NopeCharon:fix/jwt-bundle-parse-unsupported-key

Conversation

@NopeCharon

Copy link
Copy Markdown
Contributor

What

JwtBundle.parse now catches the IllegalArgumentException raised for unsupported JWKS key types (anything other than EC/RSA) and wraps it in JwtBundleException, as JwtBundle.load already does. The Javadoc now lists unsupported key types under @throws.

Fixes #479

Why

The unwrapped exception broke the documented contract of parse. On the Workload API watch path it was also thrown out of StreamObserver.onNext, because the JWT bundles observer only catches JwtBundleException. gRPC then cancelled the call with CANCELLED: Failed to read message., which is non-retryable, so the watch ended permanently and later bundle updates were never delivered.

How tested

  • JwtBundleTest.testParse_InvalidKeyType_ThrowsJwtBundleException and GrpcConversionUtilsTest.test_toJwtBundleSet_unsupportedKeyType, both using the existing jwks_invalid_keytype.json fixture. Both fail without this change.
  • Temporary test, not committed: a fake Workload API streamed a JWKS with an OKP key, then a valid JWKS, to watchJwtBundles. Before: the watcher only received Cancelling JWT Bundles watch. After: the watcher received Error processing JWT bundles update followed by the valid update.
  • ./gradlew :java-spiffe-core:build on JDK 17.

@maxlambrecht maxlambrecht left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parser fix is correct, but #479 remains unresolved through DefaultJwtSource and CachedJwtSource: their onError() callbacks throw WatcherException, which escapes onNext() and permanently cancels the stream. Could you expand this PR so rejected updates preserve existing bundles and subsequent valid updates are applied? I’d prefer addressing this here since it’s part of the reported failure, but a follow-up PR would also work.

… parse

JwtBundle.parse documents JwtBundleException for bundles that cannot be
parsed. If the JWKS contained a key whose type is neither EC nor RSA
(for example an OKP/Ed25519 or an "oct" key),
JwtSignatureAlgorithm.Family.parse threw an IllegalArgumentException that
escaped unwrapped. JwtBundle.load already catches IllegalArgumentException
for the same input, so the two entry points disagreed.

The leak was most harmful on the Workload API watch path.
StreamObservers' JWT bundles observer only catches JwtBundleException
from GrpcConversionUtils.toJwtBundleSet, so the IllegalArgumentException
was thrown out of StreamObserver.onNext. gRPC reacts by cancelling the
call ("CANCELLED: Failed to read message."). CANCELLED is treated as
non-retryable, so the watch ended permanently and later bundle updates
(rotations) were never delivered. With the exception wrapped, the
observer reports "Error processing JWT bundles update" to the watcher
and keeps the stream open for the next update.

Catch IllegalArgumentException in JwtBundle.parse as load does, and
mention unsupported key types in the Javadoc. Tests cover JwtBundle.parse
and GrpcConversionUtils.toJwtBundleSet with a JWKS that contains an OKP
key.

Signed-off-by: NopeCharon <115794254+NopeCharon@users.noreply.github.com>
DefaultJwtSource and CachedJwtSource threw a WatcherException from the
JWT bundles watcher's onError. When the error came from a rejected
update, the exception escaped StreamObserver.onNext and gRPC cancelled
the call, so the watch ended and later bundle rotations were never
applied.

Only treat errors before the first update as initialization failures:
the watcher records the error and init throws it, which also makes
newSource fail instead of returning a source without bundles when the
first update is rejected asynchronously. After initialization, errors
are logged and the current bundles are kept, so the stream stays open
and the next valid update replaces them.

Signed-off-by: NopeCharon <115794254+NopeCharon@users.noreply.github.com>
@NopeCharon
NopeCharon force-pushed the fix/jwt-bundle-parse-unsupported-key branch from f8042e5 to d4133b0 Compare October 29, 2026 17:39
@NopeCharon

Copy link
Copy Markdown
Contributor Author

@maxlambrecht Expanded it here in d4133b0: both sources now only fail on errors before the first update, and after that they log the error and keep the current bundles, so the stream stays open and the next valid update is applied. Added a test that streams an unsupported key update between two valid ones through the fake Workload API.

@maxlambrecht maxlambrecht changed the title fix(bundle): wrap unsupported JWKS key types in JwtBundleException on parse fix(workloadapi): handle rejected JWT bundle updates in JWT sources Oct 29, 2026

@maxlambrecht maxlambrecht left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks good!

@maxlambrecht
maxlambrecht merged commit c1d0f00 into spiffe:main Oct 29, 2026
5 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JwtBundle.parse leaks IllegalArgumentException for unsupported key types, cancelling the JWT bundles watch

2 participants