Skip to content

feat: add TransportOptions for configuring TLS, proxy, and default headers#103

Open
mridang wants to merge 25 commits intobetafrom
feat/add-transport-options
Open

feat: add TransportOptions for configuring TLS, proxy, and default headers#103
mridang wants to merge 25 commits intobetafrom
feat/add-transport-options

Conversation

@mridang
Copy link
Collaborator

@mridang mridang commented Mar 4, 2026

Description

This pull request adds support for transport options, allowing users to configure custom CA certificates, disable TLS verification for development environments, route requests through HTTP proxies, and inject default headers (e.g. Proxy-Authorization) into all SDK requests. A new TransportOptions class bundles these four settings into a single reusable object that can be passed to any factory method (with_access_token, with_client_credentials, with_private_key). The existing individual keyword arguments continue to work as before — no breaking changes.

Related Issue

N/A

Motivation and Context

Users deploying Zitadel behind corporate proxies, firewalls, or in environments with self-signed or private CA certificates had no clean way to configure these transport-level settings. This change gives them a single object they can construct once with their proxy URL, CA cert path, TLS preferences, and any extra headers, then pass it to whichever authentication method they use.

How Has This Been Tested

  • All existing tests pass unchanged, confirming backward compatibility with the individual-parameter approach.
  • A new integration test (test_transport_options_object) verifies that a TransportOptions instance can be passed to with_client_credentials and the SDK initializes successfully against a WireMock HTTPS endpoint.
  • The full test suite covers custom CA certs, insecure mode, default headers, proxy URLs, and the failure case when connecting to HTTPS without a trusted cert.
  • Format, lint, type checking, and dependency checks all pass.

Documentation

The README has been updated with a "Using TransportOptions" subsection under Advanced Configuration.

Checklist

  • I have updated the documentation accordingly.
  • I have assigned the correct milestone or created one if non-existent.
  • I have correctly labeled this pull request.
  • I have linked the corresponding issue in this description.
  • I have requested a review from at least 2 reviewers
  • I have checked the base branch of this pull request
  • I have checked my code for any possible security vulnerabilities

Introduce a TransportOptions dataclass that encapsulates transport-level
configuration (default_headers, ca_cert_path, insecure, proxy_url). Factory
methods now accept an optional transport_options parameter alongside the
existing individual parameters for backward compatibility. Internal auth
plumbing (OpenId, builders) refactored to use TransportOptions throughout.
@mridang mridang changed the title Add TransportOptions for configuring TLS, proxy, and default headers feat: add TransportOptions for configuring TLS, proxy, and default headers Mar 4, 2026
@mridang mridang self-assigned this Mar 4, 2026
@mridang mridang added the enhancement New feature or request label Mar 4, 2026
@mridang mridang requested a review from Copilot March 4, 2026 02:51
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a reusable TransportOptions object to configure transport-layer behavior (TLS, proxies, and headers) across Zitadel SDK factory methods and underlying HTTP plumbing.

Changes:

  • Introduces TransportOptions and wires it into Zitadel.with_* factory methods.
  • Adds support for proxy routing and default headers in the generated API client/REST layer.
  • Extends OpenID discovery to honor transport options (custom CA / insecure / proxy / headers) and adds a new integration test plus README docs.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
zitadel_client/zitadel.py Adds TransportOptions support to SDK factory methods via config mutation helpers.
zitadel_client/transport_options.py Introduces the TransportOptions dataclass.
zitadel_client/rest.py Uses urllib3.ProxyManager when Configuration.proxy_url is set.
zitadel_client/configuration.py Adds default_headers and proxy_url to configuration.
zitadel_client/api_client.py Merges Configuration.default_headers into client default headers.
zitadel_client/auth/open_id.py Applies transport options to OpenID discovery request (headers/TLS/proxy).
zitadel_client/auth/oauth_authenticator.py Threads transport options into OpenId construction.
zitadel_client/auth/client_credentials_authenticator.py Accepts/threads transport options through the builder.
zitadel_client/auth/web_token_authenticator.py Accepts/threads transport options through from_json/builder.
zitadel_client/__init__.py Exports TransportOptions at package top-level.
test/test_transport_options.py Adds integration-style coverage for transport options initialization paths.
README.md Documents advanced transport configuration and TransportOptions.
uv.lock Updates package version and dependency layout (notably cryptography).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

mridang added 4 commits March 4, 2026 14:10
frozen=True on the dataclass only prevents field reassignment but the
dict itself could still be mutated in-place. Now default_headers is
wrapped in MappingProxyType on construction, making it a read-only view.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

mridang added 9 commits March 4, 2026 18:57
Use a pre-generated keystore with proper SANs (localhost, 127.0.0.1, ::1)
for WireMock HTTPS instead of extracting certs at runtime. This fixes the
hostname mismatch error on systems where localhost resolves to IPv6.

Also threads transport options through to OAuth token exchange requests so
that custom CA, insecure mode, proxy, and default headers apply end-to-end.
… HttpWaitStrategy instead of manual polling loop\nfor WireMock readiness, consistent with Java, Node, PHP, and Ruby.
MappingProxyType from TransportOptions breaks Configuration's
deepcopy and makes headers unexpectedly immutable.
Extract duplicated session kwargs building from authenticators
into TransportOptions.to_session_kwargs().
Add WireMock stub for settings endpoint and use WireMock's
/__admin/requests/count API to assert custom headers are sent
on actual API calls, not just during initialization.
Factory methods now only accept a TransportOptions object instead of
individual default_headers, ca_cert_path, insecure, and proxy_url
parameters. This matches the Java and Node SDKs.
WireMock cannot act as an HTTP proxy for OpenID discovery, so use
with_access_token which does not trigger discovery during construction.
HttpWaitStrategy is only available in testcontainers 4.x.
Replace with manual HTTP wait using wait_container_is_ready
decorator which is available in the pinned 3.7.1 version.
Use consistent subsection structure across all SDKs: intro
paragraph, then separate sections for TLS, CA cert, headers,
and proxy with identical explanatory text.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 15 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mridang mridang force-pushed the feat/add-transport-options branch from f8c12ff to 759c9a9 Compare March 4, 2026 15:06
mridang and others added 2 commits March 5, 2026 14:45
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 17 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +240 to +275
### Custom Default Headers

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

```python
from zitadel_client import Zitadel, TransportOptions

options = TransportOptions(default_headers={"Proxy-Authorization": "Basic dXNlcjpwYXNz"})

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

### Proxy Configuration

If your environment requires routing traffic through an HTTP proxy, you can
specify the proxy URL:

```python
from zitadel_client import Zitadel, TransportOptions

options = TransportOptions(proxy_url="http://proxy:8080")

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

Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

Proxy-Authorization is documented/encouraged as a default_headers value, but default_headers are merged into the origin request headers (via ApiClient.default_headers) and will likely be forwarded to Zitadel as well, which can leak proxy credentials to the upstream service/logs. Also, urllib3 proxy authentication for HTTPS CONNECT typically uses ProxyManager(..., proxy_headers=...), not per-request headers, so this approach may not actually authenticate to the proxy. Consider documenting a safer supported pattern (e.g., embedding credentials in proxy_url) and/or introducing a dedicated proxy_headers/proxy_auth option that is applied to the proxy layer only.

Suggested change
### Custom Default Headers
You can attach default headers to every outgoing request. This is useful for
proxy authentication or custom routing headers:
```python
from zitadel_client import Zitadel, TransportOptions
options = TransportOptions(default_headers={"Proxy-Authorization": "Basic dXNlcjpwYXNz"})
zitadel = Zitadel.with_client_credentials(
"https://your-instance.zitadel.cloud",
"client-id",
"client-secret",
transport_options=options,
)
```
### Proxy Configuration
If your environment requires routing traffic through an HTTP proxy, you can
specify the proxy URL:
```python
from zitadel_client import Zitadel, TransportOptions
options = TransportOptions(proxy_url="http://proxy:8080")
zitadel = Zitadel.with_client_credentials(
"https://your-instance.zitadel.cloud",
"client-id",
"client-secret",
transport_options=options,
)
```
### Custom Default Headers
You can attach default headers to every outgoing request. This is useful for
adding custom routing headers or passing additional metadata to Zitadel:
```python
from zitadel_client import Zitadel, TransportOptions
options = TransportOptions(default_headers={"X-Custom-Header": "my-app"})
zitadel = Zitadel.with_client_credentials(
"https://your-instance.zitadel.cloud",
"client-id",
"client-secret",
transport_options=options,
)
```
### Proxy Configuration
If your environment requires routing traffic through an HTTP proxy, you can
specify the proxy URL:
```python
from zitadel_client import Zitadel, TransportOptions
options = TransportOptions(proxy_url="http://proxy:8080")
zitadel = Zitadel.with_client_credentials(
"https://your-instance.zitadel.cloud",
"client-id",
"client-secret",
transport_options=options,
)
```
> Note: Do not use `default_headers` to send `Proxy-Authorization` or other
> proxy credentials, as those headers are sent to Zitadel and may be logged
> by upstream services. If your proxy requires authentication, configure it
> using supported proxy mechanisms (for example, embedding credentials in
> `proxy_url` such as `http://user:pass@proxy:8080`), rather than origin
> request headers.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
if configuration.proxy_url:
# noinspection PyArgumentList
self.pool_manager = urllib3.ProxyManager(configuration.proxy_url, **pool_args) # ty: ignore[invalid-argument-type]
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

When proxy_url is set, urllib3.ProxyManager is created without proxy_headers. Any default_headers (including Proxy-Authorization as shown in the README) are applied to origin requests via ApiClient, but won’t be used for proxy CONNECT authentication and may be forwarded to the upstream service instead. Consider adding a dedicated proxy-auth/header setting (and passing it as proxy_headers= when creating ProxyManager) rather than relying on request headers for proxy authentication.

Suggested change
if configuration.proxy_url:
# noinspection PyArgumentList
self.pool_manager = urllib3.ProxyManager(configuration.proxy_url, **pool_args) # ty: ignore[invalid-argument-type]
proxy_headers = getattr(configuration, "proxy_headers", None)
if configuration.proxy_url:
# noinspection PyArgumentList
if proxy_headers is not None:
self.pool_manager = urllib3.ProxyManager( # ty: ignore[invalid-argument-type]
configuration.proxy_url,
proxy_headers=proxy_headers,
**pool_args,
)
else:
self.pool_manager = urllib3.ProxyManager( # ty: ignore[invalid-argument-type]
configuration.proxy_url,
**pool_args,
)

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 17 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

with urllib.request.urlopen(well_known_url) as response: # noqa S310
request = urllib.request.Request(well_known_url) # noqa: S310
if transport_options.default_headers:
for header_name, header_value in transport_options.default_headers.items():
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

TransportOptions.default_headers are applied directly to the OpenID discovery request. If a caller includes proxy-auth headers like Proxy-Authorization in default_headers, those credentials can be sent to the OpenID origin (and for HTTPS proxies they still won’t be used for proxy CONNECT auth). Consider separating proxy-auth headers from general request headers (or explicitly stripping Proxy-Authorization from the discovery request) so proxy credentials aren’t accidentally disclosed to the upstream server.

Suggested change
for header_name, header_value in transport_options.default_headers.items():
for header_name, header_value in transport_options.default_headers.items():
# Avoid sending proxy authentication headers to the upstream OpenID server
if header_name.lower() == "proxy-authorization":
continue

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +57
if configuration.proxy_url:
# noinspection PyArgumentList
self.pool_manager = urllib3.ProxyManager(configuration.proxy_url, **pool_args) # ty: ignore[invalid-argument-type]
else:
# noinspection PyArgumentList
self.pool_manager = urllib3.PoolManager(**pool_args) # ty: ignore[invalid-argument-type]
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

When using an HTTPS proxy, urllib3.ProxyManager will only send proxy-auth credentials during the CONNECT handshake if they are provided via proxy_headers (or derived from credentials embedded in the proxy URL). Any Proxy-Authorization value placed into configuration.default_headers will instead be sent as a normal request header to the origin server (over the tunnel) and won’t authenticate the proxy, which is both a functional issue and a credential-leak risk. Consider adding an explicit proxy-auth/header setting (e.g., Configuration.proxy_headers / TransportOptions.proxy_headers or a dedicated proxy_authorization field) and pass it as proxy_headers= here; also avoid forwarding that header to the upstream request headers.

Copilot uses AI. Check for mistakes.
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.

2 participants