feat: add TransportOptions for configuring TLS, proxy, and default headers#103
feat: add TransportOptions for configuring TLS, proxy, and default headers#103
Conversation
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.
There was a problem hiding this comment.
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
TransportOptionsand wires it intoZitadel.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.
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.
There was a problem hiding this comment.
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.
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.
There was a problem hiding this comment.
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.
f8c12ff to
759c9a9
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
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.
| ### 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, | ||
| ) | ||
| ``` | ||
|
|
There was a problem hiding this comment.
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.
| ### 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. |
| if configuration.proxy_url: | ||
| # noinspection PyArgumentList | ||
| self.pool_manager = urllib3.ProxyManager(configuration.proxy_url, **pool_args) # ty: ignore[invalid-argument-type] |
There was a problem hiding this comment.
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.
| 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, | |
| ) |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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.
| 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 |
| 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] |
There was a problem hiding this comment.
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.
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 newTransportOptionsclass 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
test_transport_options_object) verifies that aTransportOptionsinstance can be passed towith_client_credentialsand the SDK initializes successfully against a WireMock HTTPS endpoint.Documentation
The README has been updated with a "Using TransportOptions" subsection under Advanced Configuration.
Checklist