Skip to content

Commit fa167da

Browse files
committed
feat: add TransportOptions for configuring TLS, proxy, and default headers (#79)
1 parent f6958c8 commit fa167da

21 files changed

+593
-71
lines changed

.releaserc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
]
5757
}
5858
],
59-
"semantic-release-rubygem",
59+
"@webhippie/semantic-release-rubygem",
6060
[
6161
"@semantic-release/git",
6262
{

README.md

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,22 +194,99 @@ environment and security requirements. For more details, please refer to the
194194
### Debugging
195195

196196
The SDK supports debug logging, which can be enabled for troubleshooting
197-
and debugging purposes. You can enable debug logging by setting the `debug`
198-
flag to `true` when initializing the `Zitadel` client, like this:
197+
and debugging purposes. You can enable debug logging by setting `debugging`
198+
to `true` via the configuration block when initializing the `Zitadel` client:
199199

200200
```ruby
201-
zitadel = zitadel.Zitadel("your-zitadel-base-url", 'your-valid-token', lambda config: config.debug = True)
201+
zitadel = Zitadel::Client::Zitadel.with_access_token(
202+
'your-zitadel-base-url',
203+
'your-valid-token'
204+
) do |config|
205+
config.debugging = true
206+
end
202207
```
203208

204209
When enabled, the SDK will log additional information, such as HTTP request
205210
and response details, which can be useful for identifying issues in the
206211
integration or troubleshooting unexpected behavior.
207212

213+
## Advanced Configuration
214+
215+
The SDK provides a `TransportOptions` object that allows you to customise
216+
the underlying HTTP transport used for both OpenID discovery and API calls.
217+
218+
### Disabling TLS Verification
219+
220+
In development or testing environments with self-signed certificates, you can
221+
disable TLS verification entirely:
222+
223+
```ruby
224+
options = Zitadel::Client::TransportOptions.new(insecure: true)
225+
226+
zitadel = Zitadel::Client::Zitadel.with_client_credentials(
227+
'https://your-instance.zitadel.cloud',
228+
'client-id',
229+
'client-secret',
230+
transport_options: options
231+
)
232+
```
233+
234+
### Using a Custom CA Certificate
235+
236+
If your Zitadel instance uses a certificate signed by a private CA, you can
237+
provide the path to the CA certificate in PEM format:
238+
239+
```ruby
240+
options = Zitadel::Client::TransportOptions.new(ca_cert_path: '/path/to/ca.pem')
241+
242+
zitadel = Zitadel::Client::Zitadel.with_client_credentials(
243+
'https://your-instance.zitadel.cloud',
244+
'client-id',
245+
'client-secret',
246+
transport_options: options
247+
)
248+
```
249+
250+
### Custom Default Headers
251+
252+
You can attach default headers to every outgoing request. This is useful for
253+
custom routing or tracing headers:
254+
255+
```ruby
256+
options = Zitadel::Client::TransportOptions.new(
257+
default_headers: { 'X-Custom-Header' => 'my-value' }
258+
)
259+
260+
zitadel = Zitadel::Client::Zitadel.with_client_credentials(
261+
'https://your-instance.zitadel.cloud',
262+
'client-id',
263+
'client-secret',
264+
transport_options: options
265+
)
266+
```
267+
268+
### Proxy Configuration
269+
270+
If your environment requires routing traffic through an HTTP proxy, you can
271+
specify the proxy URL. To authenticate with the proxy, embed the credentials
272+
directly in the URL:
273+
274+
```ruby
275+
options = Zitadel::Client::TransportOptions.new(proxy_url: 'http://user:pass@proxy:8080')
276+
277+
zitadel = Zitadel::Client::Zitadel.with_client_credentials(
278+
'https://your-instance.zitadel.cloud',
279+
'client-id',
280+
'client-secret',
281+
transport_options: options
282+
)
283+
```
284+
208285
## Design and Dependencies
209286

210287
This SDK is designed to be lean and efficient, focusing on providing a
211288
streamlined way to interact with the Zitadel API. It relies on the commonly used
212-
urllib3 HTTP transport for making requests, which ensures that
289+
Typhoeus HTTP library for making requests, which ensures that
213290
the SDK integrates well with other libraries and provides flexibility
214291
in terms of request handling and error management.
215292

lib/zitadel/client/api_client.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def initialize(config = Configuration.new)
4848
@default_headers = {
4949
'Content-Type' => 'application/json',
5050
'User-Agent' => config.user_agent
51-
}
51+
}.merge(config.default_headers || {})
5252
end
5353

5454
# noinspection RubyClassVariableUsageInspection,RbsMissingTypeSignature
@@ -114,6 +114,9 @@ def build_request(http_method, path, opts = {})
114114
# set custom cert, if provided
115115
req_opts[:cainfo] = @config.ssl_ca_cert if @config.ssl_ca_cert
116116

117+
# set proxy, if provided
118+
req_opts[:proxy] = @config.proxy_url if @config.proxy_url
119+
117120
if %i[post patch put delete].include?(http_method)
118121
req_body = build_request_body(header_params, form_params, opts[:body])
119122
req_opts.update body: req_body

lib/zitadel/client/auth/authenticator.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,12 @@ class OAuthAuthenticatorBuilder
5656
# Initializes the OAuthAuthenticatorBuilder with a given host.
5757
#
5858
# @param host [String] the base URL for the OAuth provider.
59+
# @param transport_options [TransportOptions, nil] Optional transport options for TLS, proxy, and headers.
5960
#
60-
def initialize(host)
61-
@open_id = OpenId.new(host)
61+
def initialize(host, transport_options: nil)
62+
transport_options ||= TransportOptions.defaults
63+
@transport_options = transport_options
64+
@open_id = OpenId.new(host, transport_options: transport_options)
6265
@auth_scopes = Set.new(%w[openid urn:zitadel:iam:org:project:id:zitadel:aud])
6366
end
6467

lib/zitadel/client/auth/client_credentials_authenticator.rb

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,30 @@ class ClientCredentialsAuthenticator < Auth::OAuthAuthenticator
1111
# @param client_id [String] The OAuth client identifier.
1212
# @param client_secret [String] The OAuth client secret.
1313
# @param auth_scopes [Set<String>] The scope(s) for the token request.
14-
def initialize(open_id, client_id, client_secret, auth_scopes)
14+
# @param transport_options [TransportOptions, nil] Optional transport options for TLS, proxy, and headers.
15+
def initialize(open_id, client_id, client_secret, auth_scopes, transport_options: nil)
16+
transport_options ||= TransportOptions.defaults
17+
18+
conn_opts = transport_options.to_connection_opts
19+
1520
# noinspection RubyArgCount
1621
super(open_id, auth_scopes, OAuth2::Client.new(client_id, client_secret, {
1722
site: open_id.host_endpoint,
18-
token_url: open_id.token_endpoint
19-
}))
23+
token_url: open_id.token_endpoint,
24+
connection_opts: conn_opts
25+
}), transport_options: transport_options)
2026
end
2127

2228
# Returns a new builder for constructing a ClientCredentialsAuthenticator.
2329
#
2430
# @param host [String] The OAuth provider's base URL.
2531
# @param client_id [String] The OAuth client identifier.
2632
# @param client_secret [String] The OAuth client secret.
33+
# @param transport_options [TransportOptions, nil] Optional transport options for TLS, proxy, and headers.
2734
# @return [ClientCredentialsAuthenticatorBuilder] A builder instance.
28-
def self.builder(host, client_id, client_secret)
29-
ClientCredentialsAuthenticatorBuilder.new(host, client_id, client_secret)
35+
def self.builder(host, client_id, client_secret, transport_options: nil)
36+
ClientCredentialsAuthenticatorBuilder.new(host, client_id, client_secret,
37+
transport_options: transport_options)
3038
end
3139

3240
protected
@@ -45,9 +53,10 @@ class ClientCredentialsAuthenticatorBuilder < OAuthAuthenticatorBuilder
4553
# @param host [String] The OAuth provider's base URL.
4654
# @param client_id [String] The OAuth client identifier.
4755
# @param client_secret [String] The OAuth client secret.
48-
def initialize(host, client_id, client_secret)
56+
# @param transport_options [TransportOptions, nil] Optional transport options for TLS, proxy, and headers.
57+
def initialize(host, client_id, client_secret, transport_options: nil)
4958
# noinspection RubyArgCount
50-
super(host)
59+
super(host, transport_options: transport_options)
5160
@client_id = client_id
5261
@client_secret = client_secret
5362
end
@@ -56,7 +65,8 @@ def initialize(host, client_id, client_secret)
5665
#
5766
# @return [ClientCredentialsAuthenticator] A configured instance.
5867
def build
59-
ClientCredentialsAuthenticator.new(open_id, @client_id, @client_secret, auth_scopes)
68+
ClientCredentialsAuthenticator.new(open_id, @client_id, @client_secret, auth_scopes,
69+
transport_options: @transport_options)
6070
end
6171
end
6272
end

lib/zitadel/client/auth/o_auth_authenticator.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ class OAuthAuthenticator < Authenticator
2525
# Constructs an OAuthAuthenticator.
2626
#
2727
# @param open_id [OpenId] An object that must implement `get_host_endpoint` and `get_token_endpoint`.
28-
# @param auth_session [OAuth2Session] The OAuth2Session instance used for token requests.
28+
# @param auth_scopes [Set<String>] The scope(s) for the token request.
29+
# @param auth_session [OAuth2::Client] The OAuth2 client instance used for token requests.
30+
# @param transport_options [TransportOptions, nil] Optional transport options for TLS, proxy, and headers.
2931
#
30-
def initialize(open_id, auth_scopes, auth_session)
32+
def initialize(open_id, auth_scopes, auth_session, transport_options: nil)
3133
super(open_id.host_endpoint)
3234
@open_id = open_id
35+
@transport_options = transport_options || TransportOptions.defaults
3336
@token = nil
3437
@auth_session = auth_session
3538
@auth_scopes = auth_scopes.to_a.join(' ')

lib/zitadel/client/auth/open_id.rb

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'json'
44
require 'uri'
55
require 'net/http'
6+
require 'openssl'
67

78
module Zitadel
89
module Client
@@ -20,16 +21,38 @@ class OpenId
2021
# Initializes a new OpenId instance.
2122
#
2223
# @param hostname [String] the hostname for the OpenID provider.
24+
# @param transport_options [TransportOptions, nil] Optional transport options for TLS, proxy, and headers.
2325
# @raise [RuntimeError] if the OpenID configuration cannot be fetched or the token_endpoint is missing.
2426
#
2527
# noinspection HttpUrlsUsage
26-
def initialize(hostname)
28+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
29+
def initialize(hostname, transport_options: nil)
30+
transport_options ||= TransportOptions.defaults
2731
hostname = "https://#{hostname}" unless hostname.start_with?('http://', 'https://')
2832
@host_endpoint = hostname
2933
well_known_url = self.class.build_well_known_url(hostname)
3034

3135
uri = URI.parse(well_known_url)
32-
response = Net::HTTP.get_response(uri)
36+
http = if transport_options.proxy_url
37+
proxy_uri = URI.parse(transport_options.proxy_url)
38+
Net::HTTP.new(uri.host.to_s, uri.port, proxy_uri.host, proxy_uri.port,
39+
proxy_uri.user, proxy_uri.password)
40+
else
41+
Net::HTTP.new(uri.host.to_s, uri.port)
42+
end
43+
http.use_ssl = (uri.scheme == 'https')
44+
if transport_options.insecure
45+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
46+
elsif transport_options.ca_cert_path
47+
store = OpenSSL::X509::Store.new
48+
store.set_default_paths
49+
store.add_file(transport_options.ca_cert_path)
50+
http.cert_store = store
51+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
52+
end
53+
request = Net::HTTP::Get.new(uri)
54+
transport_options.default_headers.each { |k, v| request[k] = v }
55+
response = http.request(request)
3356
raise "Failed to fetch OpenID configuration: HTTP #{response.code}" unless response.code.to_i == 200
3457

3558
config = JSON.parse(response.body)
@@ -38,6 +61,7 @@ def initialize(hostname)
3861

3962
@token_endpoint = token_endpoint
4063
end
64+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
4165

4266
##
4367
# Builds the well-known OpenID configuration URL for the given hostname.

lib/zitadel/client/auth/web_token_authenticator.rb

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,20 @@ class WebTokenAuthenticator < Auth::OAuthAuthenticator
2525
# @param jwt_lifetime [Integer] Lifetime of the JWT in seconds (default 3600 seconds).
2626
# @param jwt_algorithm [String] The JWT signing algorithm (default "RS256").
2727
# @param key_id [String, nil] Optional key identifier for the JWT header (default: nil).
28-
# rubocop:disable Metrics/ParameterLists,Metrics/MethodLength
28+
# @param transport_options [TransportOptions, nil] Optional transport options for TLS, proxy, and headers.
29+
# rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
2930
def initialize(open_id, auth_scopes, jwt_issuer, jwt_subject, jwt_audience, private_key,
30-
jwt_lifetime: 3600, jwt_algorithm: 'RS256', key_id: nil)
31+
jwt_lifetime: 3600, jwt_algorithm: 'RS256', key_id: nil, transport_options: nil)
32+
transport_options ||= TransportOptions.defaults
33+
34+
conn_opts = transport_options.to_connection_opts
35+
3136
# noinspection RubyArgCount,RubyMismatchedArgumentType
3237
super(open_id, auth_scopes, OAuth2::Client.new('zitadel', 'zitadel', {
3338
site: open_id.host_endpoint,
34-
token_url: open_id.token_endpoint
35-
}))
39+
token_url: open_id.token_endpoint,
40+
connection_opts: conn_opts
41+
}), transport_options: transport_options)
3642
@jwt_issuer = jwt_issuer
3743
@jwt_subject = jwt_subject
3844
@jwt_audience = jwt_audience
@@ -47,7 +53,7 @@ def initialize(open_id, auth_scopes, jwt_issuer, jwt_subject, jwt_audience, priv
4753
end
4854
end
4955

50-
# rubocop:enable Metrics/ParameterLists,Metrics/MethodLength
56+
# rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
5157

5258
# Creates a WebTokenAuthenticator instance from a JSON configuration file.
5359
#
@@ -62,9 +68,11 @@ def initialize(open_id, auth_scopes, jwt_issuer, jwt_subject, jwt_audience, priv
6268
#
6369
# @param host [String] Base URL for the API endpoints.
6470
# @param json_path [String] File path to the JSON configuration file.
71+
# @param transport_options [TransportOptions, nil] Optional transport options for TLS, proxy, and headers.
6572
# @return [WebTokenAuthenticator] A new instance of WebTokenAuthenticator.
6673
# @raise [RuntimeError] If the file cannot be read, the JSON is invalid, or required keys are missing.
67-
def self.from_json(host, json_path)
74+
# rubocop:disable Metrics/MethodLength
75+
def self.from_json(host, json_path, transport_options: nil)
6876
config = JSON.parse(File.read(json_path))
6977
rescue Errno::ENOENT => e
7078
raise "Unable to read JSON file at #{json_path}: #{e.message}"
@@ -76,17 +84,21 @@ def self.from_json(host, json_path)
7684
user_id, private_key, key_id = config.values_at('userId', 'key', 'keyId')
7785
raise "Missing required keys 'userId', 'keyId' or 'key'" unless user_id && key_id && private_key
7886

79-
WebTokenAuthenticator.builder(host, user_id, private_key).key_identifier(key_id).build
87+
WebTokenAuthenticator.builder(host, user_id, private_key, transport_options: transport_options)
88+
.key_identifier(key_id).build
8089
end
90+
# rubocop:enable Metrics/MethodLength
8191

8292
# Returns a builder for constructing a WebTokenAuthenticator.
8393
#
8494
# @param host [String] The base URL for the OAuth provider.
8595
# @param user_id [String] The user identifier (used as both the issuer and subject).
8696
# @param private_key [String] The private key used to sign the JWT.
97+
# @param transport_options [TransportOptions, nil] Optional transport options for TLS, proxy, and headers.
8798
# @return [WebTokenAuthenticatorBuilder] A builder instance.
88-
def self.builder(host, user_id, private_key)
89-
WebTokenAuthenticatorBuilder.new(host, user_id, user_id, host, private_key)
99+
def self.builder(host, user_id, private_key, transport_options: nil)
100+
WebTokenAuthenticatorBuilder.new(host, user_id, user_id, host, private_key,
101+
transport_options: transport_options)
90102
end
91103

92104
protected
@@ -130,15 +142,18 @@ class WebTokenAuthenticatorBuilder < OAuthAuthenticatorBuilder
130142
# @param jwt_subject [String] The subject claim for the JWT.
131143
# @param jwt_audience [String] The audience claim for the JWT.
132144
# @param private_key [String] The PEM-formatted private key used for signing the JWT.
133-
def initialize(host, jwt_issuer, jwt_subject, jwt_audience, private_key)
145+
# @param transport_options [TransportOptions, nil] Optional transport options for TLS, proxy, and headers.
146+
# rubocop:disable Metrics/ParameterLists
147+
def initialize(host, jwt_issuer, jwt_subject, jwt_audience, private_key, transport_options: nil)
134148
# noinspection RubyArgCount
135-
super(host)
149+
super(host, transport_options: transport_options)
136150
@jwt_issuer = jwt_issuer
137151
@jwt_subject = jwt_subject
138152
@jwt_audience = jwt_audience
139153
@private_key = private_key
140154
@jwt_lifetime = 3600
141155
end
156+
# rubocop:enable Metrics/ParameterLists
142157

143158
# Sets the JWT token lifetime in seconds.
144159
#
@@ -159,7 +174,8 @@ def key_identifier(key_id)
159174
# @return [WebTokenAuthenticator] A configured instance.
160175
def build
161176
WebTokenAuthenticator.new(open_id, auth_scopes, @jwt_issuer, @jwt_subject, @jwt_audience,
162-
@private_key, jwt_lifetime: @jwt_lifetime, key_id: @key_id)
177+
@private_key, jwt_lifetime: @jwt_lifetime, key_id: @key_id,
178+
transport_options: @transport_options)
163179
end
164180
end
165181
end

0 commit comments

Comments
 (0)