Skip to content

Refactor ZtsBaseClient to an interface and add ArmeriaJwtsSigningKeyResolver#6848

Open
jrhee17 wants to merge 3 commits into
line:mainfrom
jrhee17:feat/athenz-refactor
Open

Refactor ZtsBaseClient to an interface and add ArmeriaJwtsSigningKeyResolver#6848
jrhee17 wants to merge 3 commits into
line:mainfrom
jrhee17:feat/athenz-refactor

Conversation

@jrhee17

@jrhee17 jrhee17 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

See #6847 for how this refactoring may be used.

Motivation:

ZtsBaseClient is a concrete final class, which makes it impossible to provide custom implementations (e.g., for xDS-based Athenz integration where the WebClient is constructed from a cluster snapshot rather than a ZtsBaseClientBuilder). Additionally, MinifiedAuthZpeClient extracts an SSLContext from the ClientFactory's TlsProvider to construct a JwtsSigningKeyResolver, which is fragile and tightly coupled to implementation details.

Modifications:

  • Refactored ZtsBaseClient from a final class to an interface with a single required method: webClient().
    • Existing methods (ztsUri(), proxyUri(), clientFactory(), webClient(Consumer), addTlsKeyPairListener(), removeTlsKeyPairListener()) are retained as default methods for backward compatibility.
    • Deprecated ztsUri() and proxyUri() as they leak implementation details.
  • Extracted the original implementation into a package-private DefaultZtsBaseClient.
  • Added ArmeriaJwtsSigningKeyResolver and ArmeriaResourceRetriever which use an Armeria WebClient to fetch JWKS keys, replacing the previous approach of extracting an SSLContext from the ClientFactory.
  • Simplified MinifiedAuthZpeClient to use ArmeriaJwtsSigningKeyResolver instead of manually constructing SSLContext and JwtsSigningKeyResolver.
  • Removed the proxyUri field from ZtsBaseClientBuilder as it is already applied to the ClientFactory and does not need to be stored separately.
  • Added ArmeriaJwtsSigningKeyResolverTest to verify JWKS key resolution via a mock server.

Result:

  • ZtsBaseClient can now be implemented by custom classes that provide their own WebClient, enabling use cases such as xDS-driven Athenz integration.
  • JWKS key fetching uses the same Armeria WebClient (with its TLS and proxy configuration) instead of constructing a separate SSLContext, reducing complexity and potential configuration drift.
  • No breaking changes for existing users — ZtsBaseClient.builder() continues to work as before.

@jrhee17 jrhee17 added this to the 1.41.0 milestone Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0a8386ba-6e83-468f-ae7b-751a82507e25

📥 Commits

Reviewing files that changed from the base of the PR and between 37622c6 and bf1e1ca.

📒 Files selected for processing (3)
  • athenz/src/main/java/com/linecorp/armeria/client/athenz/DefaultZtsBaseClient.java
  • athenz/src/main/java/com/linecorp/armeria/client/athenz/ZtsBaseClient.java
  • athenz/src/main/java/com/yahoo/athenz/auth/token/jwts/ArmeriaJwtsSigningKeyResolver.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • athenz/src/main/java/com/yahoo/athenz/auth/token/jwts/ArmeriaJwtsSigningKeyResolver.java
  • athenz/src/main/java/com/linecorp/armeria/client/athenz/DefaultZtsBaseClient.java

📝 Walkthrough

Walkthrough

Refactors ZtsBaseClient into an interface with a new default implementation, updates builder proxy wiring, and switches Athenz JWKS resolution to Armeria-backed WebClient retrieval with tests.

Changes

ZtsBaseClient refactor and JWKS resolver wiring

Layer / File(s) Summary
ZtsBaseClient interface contract
athenz/.../ZtsBaseClient.java
Converts ZtsBaseClient to a public interface with static builder methods and default implementations for ztsUri(), proxyUri(), clientFactory(), webClient(), and TLS listener methods.
DefaultZtsBaseClient implementation
athenz/.../DefaultZtsBaseClient.java
Adds the concrete implementation that builds a shared ClientFactory, default WebClient, TLS key-pair listener dispatch, and close handling.
ZtsBaseClientBuilder proxy and build wiring
athenz/.../ZtsBaseClientBuilder.java
Removes stored proxy state, applies proxy configuration directly to the ClientFactory, and constructs DefaultZtsBaseClient during build.
Armeria JWKS resource retrieval
athenz/.../ArmeriaResourceRetriever.java, athenz/.../ArmeriaJwtsSigningKeyResolver.java, athenz/.../jwts/package-info.java
Adds a WebClient-based JWKS retriever, a thread-local-backed signing key resolver factory, and package-level annotations/docs for the JWTS package.
MinifiedAuthZpeClient resolver wiring
athenz/.../MinifiedAuthZpeClient.java
Uses the new Armeria resolver when no JWKS URI is configured and reads an explicit proxy URI system property for the configured path.
Resolver test coverage
athenz/src/test/.../ArmeriaJwtsSigningKeyResolverTest.java
Adds tests for successful key lookup, unknown kid, and server-error handling against a mock JWKS server.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • line/armeria#6517: This PR also changes ZtsBaseClientBuilder wiring in the Athenz client construction path.
  • line/armeria#6607: This PR also modifies MinifiedAuthZpeClient.java in the same authorization area.

Suggested reviewers: trustin, minwoox

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the refactor and the new JWKS resolver added in this PR.
Description check ✅ Passed The description matches the implemented refactor, new resolver classes, and test additions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@athenz/src/main/java/com/linecorp/armeria/client/athenz/ZtsBaseClient.java`:
- Around line 112-118: The default ZtsBaseClient.webClient(Consumer<? super
WebClientBuilder>) implementation only null-checks and returns webClient(), so
it silently drops the supplied configurer despite the Javadoc implying
otherwise. Update the Javadoc on webClient(Consumer) to clearly state that the
configurer is ignored in the default method unless an implementation overrides
it, and keep DefaultZtsBaseClient as the place that actually applies the
WebClientBuilder customization.

In
`@athenz/src/main/java/com/yahoo/athenz/auth/token/jwts/ArmeriaJwtsSigningKeyResolver.java`:
- Around line 45-53: Add explicit null validation to the public factory method
create in ArmeriaJwtsSigningKeyResolver by checking both webClient and
oauth2KeysPath with Objects.requireNonNull before any use. The current
implementation can fail later with an unclear NPE at webClient.uri() or inside
ArmeriaResourceRetriever, so update create to reject null inputs up front and
keep the INIT_RETRIEVER lifecycle unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bba3bfd5-1c6f-46f9-8c4a-4ea265f9a617

📥 Commits

Reviewing files that changed from the base of the PR and between bccc765 and 34e4065.

📒 Files selected for processing (8)
  • athenz/src/main/java/com/linecorp/armeria/client/athenz/DefaultZtsBaseClient.java
  • athenz/src/main/java/com/linecorp/armeria/client/athenz/ZtsBaseClient.java
  • athenz/src/main/java/com/linecorp/armeria/client/athenz/ZtsBaseClientBuilder.java
  • athenz/src/main/java/com/linecorp/armeria/server/athenz/MinifiedAuthZpeClient.java
  • athenz/src/main/java/com/yahoo/athenz/auth/token/jwts/ArmeriaJwtsSigningKeyResolver.java
  • athenz/src/main/java/com/yahoo/athenz/auth/token/jwts/ArmeriaResourceRetriever.java
  • athenz/src/main/java/com/yahoo/athenz/auth/token/jwts/package-info.java
  • athenz/src/test/java/com/linecorp/armeria/client/athenz/ArmeriaJwtsSigningKeyResolverTest.java

Comment thread athenz/src/main/java/com/linecorp/armeria/client/athenz/ZtsBaseClient.java Outdated
@jrhee17 jrhee17 marked this pull request as ready for review July 9, 2026 02:44
@jrhee17 jrhee17 requested review from ikhoon and minwoox as code owners July 9, 2026 02:44
@mergify

mergify Bot commented Jul 9, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (8150425) to head (37622c6).
⚠️ Report is 525 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main   #6848       +/-   ##
============================================
- Coverage     74.46%       0   -74.47%     
============================================
  Files          1963       0     -1963     
  Lines         82437       0    -82437     
  Branches      10764       0    -10764     
============================================
- Hits          61385       0    -61385     
+ Misses        15918       0    -15918     
+ Partials       5134       0     -5134     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@minwoox minwoox left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍 👍 👍


@Override
public void close() {
defaultWebClient.options().factory().closeAsync();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Close clientFactory directly?

final ArmeriaResourceRetriever retriever = INIT_RETRIEVER.get();
if (retriever != null) {
return retriever;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we leave a warning log if retriever is null?

* Returns a new {@link WebClient} that can connect to the ZTS server with the specified configurer.
*/
public WebClient webClient(Consumer<? super WebClientBuilder> configurer) {
default WebClient webClient(Consumer<? super WebClientBuilder> configurer) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

configurer is ignored. Can we make this method as just a method instead of default method?

builder.alpnProtocols(SslContextUtil.DEFAULT_ALPN_PROTOCOLS);
builder.allowUnsafeCiphers(allowUnsafeCiphers);
return builder.build();
final String proxyUrl = System.getProperty(ZpeConsts.ZPE_PROP_JWK_PROXY_URI);

@ikhoon ikhoon Jul 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Couldn't this be a breaking change if the proxy URL specified in ZtsBaseClientBuilder is ignored?

@jrhee17 jrhee17 Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see, I wasn't sure of the intention of the original behavior either.

I was assuming that if the branch at L104 is passed, that we are defaulting back to using java properties since ZPE_PROP_JWK_URI, ZPE_PROP_JWK_PRIVATE_KEY_PATH, ZPE_PROP_JWK_X509_CERT_PATH are used.

If we should avoid a breaking change, I can revert this change. Let me know what you think

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The intention behind the existing behavior was to make ZtsBaseClient work only through Armeria’s builder API, without depending on Athenz system properties.

Although we cannot migrate every property, we intended to make the essential properties configurable through ZtsBaseClientBuilder.

For example, the proxy URL may vary depending on the user, and communication may not be possible without it. Therefore, I considered it an essential property.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just to make sure we're on the same page

Using non-system properties, we already uses armeria's proxy mechanism with the current proposed changes

Using java properties, we should still ignore other properties set on the builder except for proxyUrl

Is my understanding correct?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I assumed we could reuse the existing proxy value, since the proxy had already been configured for communication with the ZTS server. I also thought that having multiple proxy servers would be uncommon.

As for the other system properties, I thought it would be fine to keep the upstream behavior as is, since there were no conflicting settings.

That said, after searching the internal repositories, I found that ZPE_PROP_JWK_PROXY_URI and athenz.zpe.jwk_proxy_uri do not appear to be used anywhere, so I believe this change will not have any impact. Given that, I think we can keep the current implementation instead of reverting it.

By the way, why don't we use ArmeriaJwtsSigningKeyResolver in this case?


clientBuilder.factory(clientFactory);

if (LoggerFactory.getLogger(DefaultZtsBaseClient.class).isTraceEnabled()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Chaining the logger name would be a weak breaking change.

Suggested change
if (LoggerFactory.getLogger(DefaultZtsBaseClient.class).isTraceEnabled()) {
if (LoggerFactory.getLogger(ZtsBaseClient.class).isTraceEnabled()) {

@ikhoon ikhoon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants