documentation notes in code comments and README highlighting OAuth 2.1 differences, with references, such as:
- PKCE required for auth code,
@@ -1245,7 +1248,7 @@
diff --git a/docs/file.CITATION.html b/docs/file.CITATION.html
index 6b826eb1..571eccfa 100644
--- a/docs/file.CITATION.html
+++ b/docs/file.CITATION.html
@@ -82,7 +82,7 @@
diff --git a/docs/file.CODE_OF_CONDUCT.html b/docs/file.CODE_OF_CONDUCT.html
index f6e031d6..b68c6647 100644
--- a/docs/file.CODE_OF_CONDUCT.html
+++ b/docs/file.CODE_OF_CONDUCT.html
@@ -191,7 +191,7 @@ Attribution
diff --git a/docs/file.CONTRIBUTING.html b/docs/file.CONTRIBUTING.html
index 6b6331c4..33182acd 100644
--- a/docs/file.CONTRIBUTING.html
+++ b/docs/file.CONTRIBUTING.html
@@ -274,7 +274,7 @@ Manual process
diff --git a/docs/file.FUNDING.html b/docs/file.FUNDING.html
index 40725527..60c5082a 100644
--- a/docs/file.FUNDING.html
+++ b/docs/file.FUNDING.html
@@ -104,7 +104,7 @@ Another Way to Support Open
diff --git a/docs/file.LICENSE.html b/docs/file.LICENSE.html
index a07d6c0e..0cecdb01 100644
--- a/docs/file.LICENSE.html
+++ b/docs/file.LICENSE.html
@@ -60,7 +60,7 @@
MIT License
Copyright (c) 2017-2025 Peter H. Boling, of Galtzo.com, and oauth2 contributors
Copyright (c) 2011-2013 Michael Bleigh and Intridea, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
diff --git a/docs/file.OIDC.html b/docs/file.OIDC.html
index 6d44d273..e6c66e82 100644
--- a/docs/file.OIDC.html
+++ b/docs/file.OIDC.html
@@ -247,7 +247,7 @@ Raw OIDC with ruby-oauth/oauth2
diff --git a/docs/file.README.html b/docs/file.README.html
index d8f9587c..893af48b 100644
--- a/docs/file.README.html
+++ b/docs/file.README.html
@@ -1053,10 +1053,63 @@ Token Revocation (RFC 7009)
Client Configuration Tips
+Mutual TLS (mTLS) client authentication
+
+
Some providers require OAuth requests (including the token request and subsequent API calls) to be sender‑constrained using mutual TLS (mTLS). With this gem, you enable mTLS by providing a client certificate/private key to Faraday via connection_opts.ssl and, if your provider requires it for client authentication, selecting the tls_client_auth auth_scheme.
+
+Example using PEM files (certificate and key):
+
+
require "oauth2"
+require "openssl"
+
+client = OAuth2::Client.new(
+ ENV.fetch("CLIENT_ID"),
+ ENV.fetch("CLIENT_SECRET"),
+ site: "https://example.com",
+ authorize_url: "/oauth/authorize/",
+ token_url: "/oauth/token/",
+ auth_scheme: :tls_client_auth, # if your AS requires mTLS-based client authentication
+ connection_opts: {
+ ssl: {
+ client_cert: OpenSSL::X509::Certificate.new(File.read("localhost.pem")),
+ client_key: OpenSSL::PKey::RSA.new(File.read("localhost-key.pem")),
+ # Optional extras, uncomment as needed:
+ # ca_file: "/path/to/ca-bundle.pem", # custom CA(s)
+ # verify: true # enable server cert verification (recommended)
+ },
+ },
+)
+
+# Example token request (any grant type can be used). The mTLS handshake
+# will occur automatically on HTTPS calls using the configured cert/key.
+access = client.client_credentials.get_token
+
+# Subsequent resource requests will also use mTLS on HTTPS endpoints of `site`:
+resp = access.get("/v1/protected")
+
+
+Notes:
- - Authentication schemes for the token request:
+ - Files must contain the appropriate PEMs. The private key may be encrypted; if so, pass a password to OpenSSL::PKey::RSA.new(File.read(path), ENV[“KEY_PASSWORD”]).
+ - If your certificate and key are in a PKCS#12/PFX bundle, you can load them like:
+
+ - p12 = OpenSSL::PKCS12.new(File.read(“client.p12”), ENV[“P12_PASSWORD”])
+ - client_cert = p12.certificate; client_key = p12.key
+
+
+ - Server trust:
+
+ - If your environment does not have system CAs, specify ca_file or ca_path inside the ssl: hash.
+ - Keep verify: true in production. Set verify: false only for local testing.
+
+
+ - Faraday adapter: Any adapter that supports Ruby’s OpenSSL should work. net_http (default) and net_http_persistent are common choices.
+ - Scope of mTLS: The SSL client cert is applied to any HTTPS request made by this client (token and resource requests) to the configured site base URL (and absolute URLs you call with the same client).
+ - OIDC tie-in: Some OPs require tls_client_auth at the token endpoint per OIDC/OAuth specifications. That is enabled via auth_scheme: :tls_client_auth as shown above.
+Authentication schemes for the token request
+
OAuth2::Client.new(
id,
secret,
@@ -1065,9 +1118,7 @@ Client Configuration Tips
)
-
- - Faraday connection, timeouts, proxy, custom adapter/middleware:
-
+Faraday connection, timeouts, proxy, custom adapter/middleware:
client = OAuth2::Client.new(
id,
@@ -1085,9 +1136,50 @@ Client Configuration Tips
end
-
- - Redirection: The library follows up to
max_redirects (default 5). You can override per-client via options[:max_redirects].
-
+Using flat query params (Faraday::FlatParamsEncoder)
+
+Some APIs expect repeated key parameters to be sent as flat params rather than arrays. Faraday provides FlatParamsEncoder for this purpose. You can configure the oauth2 client to use it when building requests.
+
+require "faraday"
+
+client = OAuth2::Client.new(
+ id,
+ secret,
+ site: "https://api.example.com",
+ # Pass Faraday connection options to make FlatParamsEncoder the default
+ connection_opts: {
+ request: {params_encoder: Faraday::FlatParamsEncoder},
+ },
+) do |faraday|
+ faraday.request(:url_encoded)
+ faraday.adapter(:net_http)
+end
+
+access = client.client_credentials.get_token
+
+# Example of a GET with two flat filter params (not an array):
+# Results in: ?filter=order.clientCreatedTime%3E1445006997000&filter=order.clientCreatedTime%3C1445611797000
+resp = access.get(
+ "/v1/orders",
+ params: {
+ # Provide the values as an array; FlatParamsEncoder expands them as repeated keys
+ filter: [
+ "order.clientCreatedTime>1445006997000",
+ "order.clientCreatedTime<1445611797000",
+ ],
+ },
+)
+
+
+If you instead need to build a raw Faraday connection yourself, the equivalent configuration is:
+
+conn = Faraday.new("https://api.example.com", request: {params_encoder: Faraday::FlatParamsEncoder})
+
+
+Redirection
+
+The library follows up to max_redirects (default 5).
+You can override per-client via options[:max_redirects].
Handling Responses and Errors
@@ -1376,7 +1468,7 @@ Please give the project a star ⭐ ♥
diff --git a/docs/file.REEK.html b/docs/file.REEK.html
index 90047ac4..b064d230 100644
--- a/docs/file.REEK.html
+++ b/docs/file.REEK.html
@@ -61,7 +61,7 @@
diff --git a/docs/file.RUBOCOP.html b/docs/file.RUBOCOP.html
index 2b54635d..ee1b3370 100644
--- a/docs/file.RUBOCOP.html
+++ b/docs/file.RUBOCOP.html
@@ -161,7 +161,7 @@ Benefits of rubocop_gradual
diff --git a/docs/file.SECURITY.html b/docs/file.SECURITY.html
index 5543e445..53ad1d22 100644
--- a/docs/file.SECURITY.html
+++ b/docs/file.SECURITY.html
@@ -113,7 +113,7 @@ Enterprise Support
diff --git a/docs/file.access_token.html b/docs/file.access_token.html
index f5c107c0..e947e9b5 100644
--- a/docs/file.access_token.html
+++ b/docs/file.access_token.html
@@ -84,7 +84,7 @@
diff --git a/docs/file.authenticator.html b/docs/file.authenticator.html
index 92e43342..b55ae39b 100644
--- a/docs/file.authenticator.html
+++ b/docs/file.authenticator.html
@@ -81,7 +81,7 @@
diff --git a/docs/file.client.html b/docs/file.client.html
index 54a8ee04..0b92d6ca 100644
--- a/docs/file.client.html
+++ b/docs/file.client.html
@@ -111,7 +111,7 @@
diff --git a/docs/file.error.html b/docs/file.error.html
index a67057bf..8548b09b 100644
--- a/docs/file.error.html
+++ b/docs/file.error.html
@@ -68,7 +68,7 @@
diff --git a/docs/file.filtered_attributes.html b/docs/file.filtered_attributes.html
index 321800fb..83557a79 100644
--- a/docs/file.filtered_attributes.html
+++ b/docs/file.filtered_attributes.html
@@ -66,7 +66,7 @@
diff --git a/docs/file.oauth2-2.0.10.gem.html b/docs/file.oauth2-2.0.10.gem.html
index f2067f05..2ffc9eda 100644
--- a/docs/file.oauth2-2.0.10.gem.html
+++ b/docs/file.oauth2-2.0.10.gem.html
@@ -61,7 +61,7 @@
diff --git a/docs/file.oauth2-2.0.11.gem.html b/docs/file.oauth2-2.0.11.gem.html
index 0de82d22..da2c6757 100644
--- a/docs/file.oauth2-2.0.11.gem.html
+++ b/docs/file.oauth2-2.0.11.gem.html
@@ -61,7 +61,7 @@
diff --git a/docs/file.oauth2-2.0.12.gem.html b/docs/file.oauth2-2.0.12.gem.html
index a68ef53b..340caa14 100644
--- a/docs/file.oauth2-2.0.12.gem.html
+++ b/docs/file.oauth2-2.0.12.gem.html
@@ -61,7 +61,7 @@
diff --git a/docs/file.oauth2-2.0.13.gem.html b/docs/file.oauth2-2.0.13.gem.html
index b709b725..d18bc2e8 100644
--- a/docs/file.oauth2-2.0.13.gem.html
+++ b/docs/file.oauth2-2.0.13.gem.html
@@ -61,7 +61,7 @@
diff --git a/docs/file.oauth2.html b/docs/file.oauth2.html
index 13aa7462..4f8019ea 100644
--- a/docs/file.oauth2.html
+++ b/docs/file.oauth2.html
@@ -69,7 +69,7 @@
diff --git a/docs/file.response.html b/docs/file.response.html
index 4d995be8..e148314f 100644
--- a/docs/file.response.html
+++ b/docs/file.response.html
@@ -77,7 +77,7 @@
diff --git a/docs/file.strategy.html b/docs/file.strategy.html
index 4a213119..1bda5575 100644
--- a/docs/file.strategy.html
+++ b/docs/file.strategy.html
@@ -93,7 +93,7 @@
diff --git a/docs/file.version.html b/docs/file.version.html
index 40991e29..59dd25c8 100644
--- a/docs/file.version.html
+++ b/docs/file.version.html
@@ -65,7 +65,7 @@
diff --git a/docs/index.html b/docs/index.html
index d06c59d7..9230e816 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1053,10 +1053,63 @@ Token Revocation (RFC 7009)
Client Configuration Tips
+Mutual TLS (mTLS) client authentication
+
+
Some providers require OAuth requests (including the token request and subsequent API calls) to be sender‑constrained using mutual TLS (mTLS). With this gem, you enable mTLS by providing a client certificate/private key to Faraday via connection_opts.ssl and, if your provider requires it for client authentication, selecting the tls_client_auth auth_scheme.
+
+Example using PEM files (certificate and key):
+
+require "oauth2"
+require "openssl"
+
+client = OAuth2::Client.new(
+ ENV.fetch("CLIENT_ID"),
+ ENV.fetch("CLIENT_SECRET"),
+ site: "https://example.com",
+ authorize_url: "/oauth/authorize/",
+ token_url: "/oauth/token/",
+ auth_scheme: :tls_client_auth, # if your AS requires mTLS-based client authentication
+ connection_opts: {
+ ssl: {
+ client_cert: OpenSSL::X509::Certificate.new(File.read("localhost.pem")),
+ client_key: OpenSSL::PKey::RSA.new(File.read("localhost-key.pem")),
+ # Optional extras, uncomment as needed:
+ # ca_file: "/path/to/ca-bundle.pem", # custom CA(s)
+ # verify: true # enable server cert verification (recommended)
+ },
+ },
+)
+
+# Example token request (any grant type can be used). The mTLS handshake
+# will occur automatically on HTTPS calls using the configured cert/key.
+access = client.client_credentials.get_token
+
+# Subsequent resource requests will also use mTLS on HTTPS endpoints of `site`:
+resp = access.get("/v1/protected")
+
+
+Notes:
- - Authentication schemes for the token request:
+ - Files must contain the appropriate PEMs. The private key may be encrypted; if so, pass a password to OpenSSL::PKey::RSA.new(File.read(path), ENV[“KEY_PASSWORD”]).
+ - If your certificate and key are in a PKCS#12/PFX bundle, you can load them like:
+
+ - p12 = OpenSSL::PKCS12.new(File.read(“client.p12”), ENV[“P12_PASSWORD”])
+ - client_cert = p12.certificate; client_key = p12.key
+
+
+ - Server trust:
+
+ - If your environment does not have system CAs, specify ca_file or ca_path inside the ssl: hash.
+ - Keep verify: true in production. Set verify: false only for local testing.
+
+
+ - Faraday adapter: Any adapter that supports Ruby’s OpenSSL should work. net_http (default) and net_http_persistent are common choices.
+ - Scope of mTLS: The SSL client cert is applied to any HTTPS request made by this client (token and resource requests) to the configured site base URL (and absolute URLs you call with the same client).
+ - OIDC tie-in: Some OPs require tls_client_auth at the token endpoint per OIDC/OAuth specifications. That is enabled via auth_scheme: :tls_client_auth as shown above.
+Authentication schemes for the token request
+
OAuth2::Client.new(
id,
secret,
@@ -1065,9 +1118,7 @@ Client Configuration Tips
)
-
- - Faraday connection, timeouts, proxy, custom adapter/middleware:
-
+Faraday connection, timeouts, proxy, custom adapter/middleware:
client = OAuth2::Client.new(
id,
@@ -1085,9 +1136,50 @@ Client Configuration Tips
end
-
- - Redirection: The library follows up to
max_redirects (default 5). You can override per-client via options[:max_redirects].
-
+Using flat query params (Faraday::FlatParamsEncoder)
+
+Some APIs expect repeated key parameters to be sent as flat params rather than arrays. Faraday provides FlatParamsEncoder for this purpose. You can configure the oauth2 client to use it when building requests.
+
+require "faraday"
+
+client = OAuth2::Client.new(
+ id,
+ secret,
+ site: "https://api.example.com",
+ # Pass Faraday connection options to make FlatParamsEncoder the default
+ connection_opts: {
+ request: {params_encoder: Faraday::FlatParamsEncoder},
+ },
+) do |faraday|
+ faraday.request(:url_encoded)
+ faraday.adapter(:net_http)
+end
+
+access = client.client_credentials.get_token
+
+# Example of a GET with two flat filter params (not an array):
+# Results in: ?filter=order.clientCreatedTime%3E1445006997000&filter=order.clientCreatedTime%3C1445611797000
+resp = access.get(
+ "/v1/orders",
+ params: {
+ # Provide the values as an array; FlatParamsEncoder expands them as repeated keys
+ filter: [
+ "order.clientCreatedTime>1445006997000",
+ "order.clientCreatedTime<1445611797000",
+ ],
+ },
+)
+
+
+If you instead need to build a raw Faraday connection yourself, the equivalent configuration is:
+
+conn = Faraday.new("https://api.example.com", request: {params_encoder: Faraday::FlatParamsEncoder})
+
+
+Redirection
+
+The library follows up to max_redirects (default 5).
+You can override per-client via options[:max_redirects].
Handling Responses and Errors
@@ -1376,7 +1468,7 @@ Please give the project a star ⭐ ♥
diff --git a/docs/top-level-namespace.html b/docs/top-level-namespace.html
index e19df3d2..804b5068 100644
--- a/docs/top-level-namespace.html
+++ b/docs/top-level-namespace.html
@@ -100,7 +100,7 @@ Defined Under Namespace
diff --git a/spec/oauth2/client_spec.rb b/spec/oauth2/client_spec.rb
index 31590250..741e7568 100644
--- a/spec/oauth2/client_spec.rb
+++ b/spec/oauth2/client_spec.rb
@@ -1339,6 +1339,37 @@ def self.contains_token?(hash)
end
end
+ context "when using Faraday::FlatParamsEncoder" do
+ before do
+ skip("Faraday::FlatParamsEncoder not available in this Faraday version") unless defined?(Faraday::FlatParamsEncoder)
+ end
+
+ it "does not discard repeated params and encodes them as flat keys" do
+ client = stubbed_client(connection_opts: {request: {params_encoder: Faraday::FlatParamsEncoder}}) do |stub|
+ stub.get("/v1/orders") do |env|
+ # Query string should contain two repeated filter keys with encoded operators
+ qs = env.url.query.to_s
+ expect(qs).to include("filter=order.clientCreatedTime%3E1445006997000")
+ expect(qs).to include("filter=order.clientCreatedTime%3C1445611797000")
+ # Ensure both occurrences exist (not collapsed)
+ expect(qs.scan(/\bfilter=/).size).to be >= 2
+ [200, {"Content-Type" => "application/json"}, JSON.dump({ok: true})]
+ end
+ end
+
+ token = OAuth2::AccessToken.new(client, "token123")
+ token.get(
+ "/v1/orders",
+ params: {
+ filter: [
+ "order.clientCreatedTime>1445006997000",
+ "order.clientCreatedTime<1445611797000",
+ ],
+ },
+ )
+ end
+ end
+
def stubbed_client(params = {}, &stubs)
params = {site: "https://api.example.com"}.merge(params)
OAuth2::Client.new("abc", "def", params) do |builder|