Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
887d007
Add TransportOptions for configuring TLS, proxy, and default headers
mridang Mar 4, 2026
0444f7f
Fix hostname verification for custom CA and address review feedback
mridang Mar 4, 2026
d3fcc5c
Merge custom CA with default trust store instead of replacing it
mridang Mar 4, 2026
31bccb0
Fix SSL socket resource leak in transport options test
mridang Mar 4, 2026
e7def42
Apply transport options to OAuth token exchange requests
mridang Mar 4, 2026
2f29aef
Fix custom CA cert test and apply transport options to token exchange
mridang Mar 4, 2026
c34990c
Standardize transport options tests across SDKs
mridang Mar 4, 2026
2bb9aa1
Add null guard for transportOptions in ApiClient constructor
mridang Mar 4, 2026
55beb47
Add null guards for transportOptions in all public constructors
mridang Mar 4, 2026
1dd95e3
Validate header name and value in TransportOptions builder
mridang Mar 4, 2026
16c7a55
Centralize SSL context building in TransportOptions
mridang Mar 4, 2026
8ee3d24
Verify default headers on API calls via WireMock verification
mridang Mar 4, 2026
728e978
Fix SpotBugs warning and proxy test reliability
mridang Mar 4, 2026
39765d7
Cache SSLContext in TransportOptions to avoid per-request rebuilds
mridang Mar 4, 2026
ef23bed
Fix install command from composer to Maven dependency
mridang Mar 4, 2026
ee976f4
fix: remove unnecessary SpotBugs suppression from buildSSLContext
mridang Mar 4, 2026
6677286
build: enable unused import removal in Spotless
mridang Mar 4, 2026
604bbd3
build: reorder Spotless steps to remove unused imports before formatting
mridang Mar 4, 2026
06365f3
fix: remove unused imports from ApiClient and OAuthAuthenticator
mridang Mar 4, 2026
6345a39
Add real proxy container to transport options test
mridang Mar 4, 2026
3d9cff2
chore: align docs and remove inline comments
mridang Mar 5, 2026
c9f4747
replace tinyproxy with ubuntu/squid:6.10-24.10_beta
mridang Mar 5, 2026
17bc888
catch GeneralSecurityException explicitly instead of broad Exception
mridang Mar 5, 2026
60db143
docs: fix proxy auth docs to use URL credentials instead of default h…
mridang Mar 5, 2026
bb84212
fix: replace broad catch(Exception) with specific checked exceptions
mridang Mar 5, 2026
fb43253
fix: remove stale SpotBugs filters for moved trust manager and except…
mridang Mar 5, 2026
2f18350
fix: parse JSON instead of fragile string matching in default headers…
mridang Mar 8, 2026
e356d03
Add missing @param transportOptions javadoc tags
mridang Mar 8, 2026
5212e15
Standardize @param transportOptions descriptions
mridang Mar 8, 2026
0f8352f
Remove TransportOptions.Builder for consistency with other SDKs
mridang Mar 8, 2026
168b357
Remove inline comments from OAuthAuthenticator
mridang Mar 8, 2026
72a11fa
Standardize cross-references and remove inline comments\n\nAdd @see l…
mridang Mar 8, 2026
e59b495
Align defaults() docstring with other SDKs\n\nUse the same summary te…
mridang Mar 8, 2026
d192f6b
Move SpotBugs suppressions from XML to code annotations\n\nReplace sp…
mridang Mar 8, 2026
f87bb27
Add backwards-compatible 2-param OAuthAuthenticator constructor
mridang Mar 9, 2026
65a8d4f
Standardize WireMock version to 3.12.1
mridang Mar 9, 2026
4d2fb51
Clarify defaultHeaders are sent to the origin server
mridang Mar 9, 2026
d4158f9
Fix stale JWTAuthenticator references in Javadoc
mridang Mar 9, 2026
50ceac4
Capitalize @param defaultHeaders description for consistency
mridang Mar 9, 2026
acce336
Replace programmatic WireMock stubs with static JSON mapping files
mridang Mar 9, 2026
8ea6f25
Standardize TransportOptions docstrings for cross-SDK consistency
mridang Mar 9, 2026
0f3dea2
Restructure tests: split TransportOptions unit tests from Zitadel int…
mridang Mar 9, 2026
4b6aa66
Harden insecure precedence test with nonexistent CA cert path
mridang Mar 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 66 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,14 @@ install dependencies.

### Installation

Install the SDK by running one of the following commands:

```bash
composer require zitadel/client
Add the SDK dependency to your `pom.xml`:

```xml
<dependency>
<groupId>io.github.zitadel</groupId>
<artifactId>client</artifactId>
<version>4.1.0</version>
Comment thread
mridang marked this conversation as resolved.
</dependency>
```

## Authentication Methods
Expand Down Expand Up @@ -193,6 +197,64 @@ Choose the authentication method that best suits your needs based on your
environment and security requirements. For more details, please refer to the
[Zitadel documentation on authenticating service users](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users).

## Advanced Configuration

The SDK provides a `TransportOptions` object that allows you to customise
the underlying HTTP transport used for both OpenID discovery and API calls.

Comment thread
mridang marked this conversation as resolved.
### Disabling TLS Verification
Comment thread
mridang marked this conversation as resolved.

In development or testing environments with self-signed certificates, you can
disable TLS verification entirely:

```java
TransportOptions options = new TransportOptions(
Map.of(), null, true, null);

Zitadel zitadel = Zitadel.withClientCredentials(
"https://your-instance.zitadel.cloud", "client-id", "client-secret", options);
```

### Using a Custom CA Certificate

If your Zitadel instance uses a certificate signed by a private CA, you can
provide the path to the CA certificate in PEM format:

```java
TransportOptions options = new TransportOptions(
Map.of(), "/path/to/ca.pem", false, null);

Zitadel zitadel = Zitadel.withClientCredentials(
"https://your-instance.zitadel.cloud", "client-id", "client-secret", options);
```

### Custom Default Headers

You can attach default headers to every outgoing request. This is useful for
custom routing or tracing headers:

```java
TransportOptions options = new TransportOptions(
Map.of("X-Custom-Header", "my-value"), null, false, null);

Zitadel zitadel = Zitadel.withClientCredentials(
"https://your-instance.zitadel.cloud", "client-id", "client-secret", options);
```

### Proxy Configuration

If your environment requires routing traffic through an HTTP proxy, you can
specify the proxy URL. To authenticate with the proxy, embed the credentials
directly in the URL:

```java
TransportOptions options = new TransportOptions(
Map.of(), null, false, "http://user:pass@proxy:8080");

Zitadel zitadel = Zitadel.withClientCredentials(
"https://your-instance.zitadel.cloud", "client-id", "client-secret", options);
```

## Design and Dependencies

This SDK is designed to be lean and efficient, focusing on providing a
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@
<exclude>**/api/*.java</exclude>
<exclude>**/model/*.java</exclude>
</excludes>
<removeUnusedImports/>
<googleJavaFormat/>
</java>
</configuration>
Expand Down
62 changes: 61 additions & 1 deletion src/main/java/com/zitadel/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,27 @@
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.core5.http.*;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.FileEntity;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.openapitools.jackson.nullable.JsonNullableModule;

import javax.annotation.Nullable;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.lang.reflect.Type;
import java.net.URLEncoder;
import java.nio.charset.Charset;
Expand All @@ -45,7 +52,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@SuppressFBWarnings("THROWS_METHOD_THROWS_RUNTIMEEXCEPTION")
@SuppressFBWarnings({"THROWS_METHOD_THROWS_RUNTIMEEXCEPTION", "CT_CONSTRUCTOR_THROW"})
public class ApiClient {

public static final String USER_AGENT =
Expand All @@ -68,7 +75,7 @@

public ApiClient(Authenticator authenticator, CloseableHttpClient httpClient) {
objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

Check warning on line 78 in src/main/java/com/zitadel/ApiClient.java

View workflow job for this annotation

GitHub Actions / Qodana for JVM

Deprecated API usage

'setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include)' is deprecated
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
Expand All @@ -91,6 +98,59 @@
this(authenticator, HttpClients.custom().setUserAgent(USER_AGENT).build());
}

/**
* @param authenticator the authenticator to use for API requests.
* @param transportOptions Optional transport options for TLS, proxy, and headers.
*/
public ApiClient(Authenticator authenticator, TransportOptions transportOptions) {
this(authenticator, buildHttpClient(transportOptions != null ? transportOptions : TransportOptions.defaults()));
}

private static CloseableHttpClient buildHttpClient(TransportOptions transportOptions) {
try {
HttpClientBuilder builder = HttpClients.custom().setUserAgent(USER_AGENT);

SSLContext sslContext = transportOptions.buildSSLContext();
if (sslContext != null) {
SSLConnectionSocketFactoryBuilder sslSocketFactoryBuilder = SSLConnectionSocketFactoryBuilder.create()

Check warning on line 115 in src/main/java/com/zitadel/ApiClient.java

View workflow job for this annotation

GitHub Actions / Qodana for JVM

Deprecated API usage

'org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder' is deprecated

Check warning on line 115 in src/main/java/com/zitadel/ApiClient.java

View workflow job for this annotation

GitHub Actions / Qodana for JVM

Deprecated API usage

'org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder' is deprecated
.setSslContext(sslContext);
if (transportOptions.isInsecure()) {
sslSocketFactoryBuilder.setHostnameVerifier(NoopHostnameVerifier.INSTANCE);
}
builder.setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(sslSocketFactoryBuilder.build())

Check warning on line 121 in src/main/java/com/zitadel/ApiClient.java

View workflow job for this annotation

GitHub Actions / Qodana for JVM

Deprecated API usage

'setSSLSocketFactory(org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory)' is deprecated
.build());
}

List<Header> defaultHeaders = new ArrayList<>();

if (transportOptions.getProxyUrl() != null) {
java.net.URL proxyParsed = new java.net.URL(transportOptions.getProxyUrl());
String proxyScheme = proxyParsed.getProtocol();
String proxyHost = proxyParsed.getHost();
int proxyPort = proxyParsed.getPort() != -1 ? proxyParsed.getPort() : proxyParsed.getDefaultPort();
builder.setProxy(new HttpHost(proxyScheme, proxyHost, proxyPort));
if (proxyParsed.getUserInfo() != null) {
String encoded = Base64.getEncoder()
.encodeToString(proxyParsed.getUserInfo().getBytes(StandardCharsets.UTF_8));
defaultHeaders.add(new BasicHeader("Proxy-Authorization", "Basic " + encoded));
}
}

for (Map.Entry<String, String> entry : transportOptions.getDefaultHeaders().entrySet()) {
defaultHeaders.add(new BasicHeader(entry.getKey(), entry.getValue()));
}
Comment thread
mridang marked this conversation as resolved.

if (!defaultHeaders.isEmpty()) {
builder.setDefaultHeaders(defaultHeaders);
}

return builder.build();
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException("Failed to build HTTP client with transport options: " + e.getMessage(), e);
}
}

public static DateFormat buildDefaultDateFormat() {
return new RFC3339DateFormat();
}
Expand Down Expand Up @@ -211,7 +271,7 @@
* @param value The value of the parameter.
* @return A list containing a single {@code Pair} object.
*/
public List<Pair> parameterToPair(String name, Object value) {

Check warning on line 274 in src/main/java/com/zitadel/ApiClient.java

View workflow job for this annotation

GitHub Actions / Qodana for JVM

Check Kotlin and Java source code coverage

Method `parameterToPair` coverage is below the threshold 50%
List<Pair> params = new ArrayList<>();
if (name == null || name.isEmpty() || value == null || value instanceof Collection) {
return params;
Expand Down Expand Up @@ -529,7 +589,7 @@
* @return The full URL
*/
@SuppressWarnings({"DuplicatedCode", "DuplicateExpressions"})
private String buildUrl(

Check warning on line 592 in src/main/java/com/zitadel/ApiClient.java

View workflow job for this annotation

GitHub Actions / Qodana for JVM

Check Kotlin and Java source code coverage

Method `buildUrl` coverage is below the threshold 50%
String path,
List<Pair> queryParams,
@Nullable List<Pair> collectionQueryParams,
Expand Down
173 changes: 173 additions & 0 deletions src/main/java/com/zitadel/TransportOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package com.zitadel;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import javax.annotation.Nullable;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* Immutable transport options for configuring HTTP connections.
*/
public class TransportOptions {

private final Map<String, String> defaultHeaders;
@Nullable

Check notice on line 26 in src/main/java/com/zitadel/TransportOptions.java

View workflow job for this annotation

GitHub Actions / Qodana for JVM

@NotNull/@Nullable problems

The generated code will use '@org.jetbrains.annotations.Nullable' instead of '@javax.annotation.Nullable'
private final String caCertPath;
private final boolean insecure;
@Nullable

Check notice on line 29 in src/main/java/com/zitadel/TransportOptions.java

View workflow job for this annotation

GitHub Actions / Qodana for JVM

@NotNull/@Nullable problems

The generated code will use '@org.jetbrains.annotations.Nullable' instead of '@javax.annotation.Nullable'
private final String proxyUrl;
@Nullable

Check notice on line 31 in src/main/java/com/zitadel/TransportOptions.java

View workflow job for this annotation

GitHub Actions / Qodana for JVM

@NotNull/@Nullable problems

The generated code will use '@org.jetbrains.annotations.Nullable' instead of '@javax.annotation.Nullable'
private volatile SSLContext cachedSSLContext;
private volatile boolean sslContextBuilt;

/**
* @param defaultHeaders Default HTTP headers sent to the origin server with every request.
* @param caCertPath Path to a custom CA certificate file for TLS verification, or {@code null}.
* @param insecure Whether to disable TLS certificate verification.
* @param proxyUrl Proxy URL for HTTP connections, or {@code null}.
*/
public TransportOptions(Map<String, String> defaultHeaders, @Nullable String caCertPath, boolean insecure, @Nullable String proxyUrl) {
this.defaultHeaders = Collections.unmodifiableMap(new LinkedHashMap<>(defaultHeaders));
this.caCertPath = caCertPath;
this.insecure = insecure;
this.proxyUrl = proxyUrl;
Comment thread
mridang marked this conversation as resolved.
}
Comment thread
mridang marked this conversation as resolved.

/**
* Returns a {@code TransportOptions} instance with all default values.
*
* @return the default transport options.
*/
public static TransportOptions defaults() {
return new TransportOptions(Collections.emptyMap(), null, false, null);
}

/**
* Returns the default HTTP headers sent to the origin server with every request.
*
* @return an unmodifiable map of header names to values.
*/
public Map<String, String> getDefaultHeaders() {
return defaultHeaders;
}

/**
* Returns the path to a custom CA certificate file used for TLS verification.
*
* @return the CA cert file path, or {@code null} if not set.
*/
@Nullable
public String getCaCertPath() {
return caCertPath;
}

/**
* Returns whether insecure (trust-all) TLS mode is enabled.
*
* @return {@code true} if insecure mode is enabled; {@code false} otherwise.
*/
public boolean isInsecure() {
return insecure;
}

/**
* Returns the proxy URL used for HTTP connections.
*
* @return the proxy URL, or {@code null} if not set.
*/
@Nullable
public String getProxyUrl() {
return proxyUrl;
}

/**
* Builds an {@link SSLContext} from these transport options.
*
* <p>If insecure mode is enabled, returns a trust-all SSLContext.
* If a custom CA cert path is set, returns an SSLContext that trusts
* both the custom CA and the system default CAs.
* Otherwise returns {@code null} (use JVM defaults).
*
* @return the configured SSLContext, or {@code null} if no custom SSL is needed.
* @throws GeneralSecurityException if SSL context creation fails.
* @throws IOException if the CA cert file cannot be read.
*/
@Nullable
public SSLContext buildSSLContext() throws GeneralSecurityException, IOException {
if (sslContextBuilt) {
return cachedSSLContext;
}
synchronized (this) {
if (sslContextBuilt) {
return cachedSSLContext;
}
cachedSSLContext = buildSSLContextInternal();
sslContextBuilt = true;
return cachedSSLContext;
}
}

@Nullable
@SuppressFBWarnings("PATH_TRAVERSAL_IN")
private SSLContext buildSSLContextInternal() throws GeneralSecurityException, IOException {
if (insecure) {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{new InsecureTrustManager()}, null);
return sslContext;
} else if (caCertPath != null) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
java.security.cert.Certificate caCert;
try (FileInputStream fis = new FileInputStream(caCertPath)) {
caCert = cf.generateCertificate(fis);
}
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("custom-ca", caCert);
TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
defaultTmf.init((KeyStore) null);
int certIndex = 0;
for (TrustManager tm : defaultTmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
for (X509Certificate cert : ((X509TrustManager) tm).getAcceptedIssuers()) {
ks.setCertificateEntry("default-" + certIndex++, cert);
}
}
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
return sslContext;
}
return null;
}

@SuppressFBWarnings("WEAK_TRUST_MANAGER")
private static final class InsecureTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {

Check warning on line 160 in src/main/java/com/zitadel/TransportOptions.java

View workflow job for this annotation

GitHub Actions / Qodana for JVM

Check Kotlin and Java source code coverage

Method `checkClientTrusted` coverage is below the threshold 50%
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}

}
Loading
Loading