httpx 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_11_0.md +3 -3
  3. data/doc/release_notes/1_6_3.md +47 -0
  4. data/doc/release_notes/1_7_0.md +149 -0
  5. data/lib/httpx/adapters/datadog.rb +1 -1
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/adapters/sentry.rb +1 -1
  8. data/lib/httpx/altsvc.rb +3 -1
  9. data/lib/httpx/connection/http1.rb +14 -15
  10. data/lib/httpx/connection/http2.rb +16 -15
  11. data/lib/httpx/connection.rb +118 -110
  12. data/lib/httpx/domain_name.rb +1 -1
  13. data/lib/httpx/extensions.rb +0 -14
  14. data/lib/httpx/headers.rb +2 -2
  15. data/lib/httpx/io/ssl.rb +1 -1
  16. data/lib/httpx/loggable.rb +14 -2
  17. data/lib/httpx/options.rb +60 -17
  18. data/lib/httpx/plugins/auth/digest.rb +44 -4
  19. data/lib/httpx/plugins/auth.rb +87 -4
  20. data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
  21. data/lib/httpx/plugins/callbacks.rb +15 -1
  22. data/lib/httpx/plugins/cookies/cookie.rb +1 -0
  23. data/lib/httpx/plugins/digest_auth.rb +4 -5
  24. data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
  25. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
  26. data/lib/httpx/plugins/grpc.rb +2 -2
  27. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  28. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  29. data/lib/httpx/plugins/oauth.rb +162 -56
  30. data/lib/httpx/plugins/proxy/http.rb +37 -9
  31. data/lib/httpx/plugins/rate_limiter.rb +2 -2
  32. data/lib/httpx/plugins/response_cache/file_store.rb +1 -0
  33. data/lib/httpx/plugins/response_cache.rb +16 -9
  34. data/lib/httpx/plugins/retries.rb +55 -16
  35. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  36. data/lib/httpx/plugins/stream.rb +59 -8
  37. data/lib/httpx/plugins/stream_bidi.rb +87 -22
  38. data/lib/httpx/pool.rb +65 -21
  39. data/lib/httpx/request.rb +13 -14
  40. data/lib/httpx/resolver/https.rb +100 -34
  41. data/lib/httpx/resolver/multi.rb +12 -27
  42. data/lib/httpx/resolver/native.rb +68 -38
  43. data/lib/httpx/resolver/resolver.rb +46 -29
  44. data/lib/httpx/resolver/system.rb +63 -39
  45. data/lib/httpx/resolver.rb +97 -29
  46. data/lib/httpx/response/body.rb +2 -0
  47. data/lib/httpx/response.rb +22 -6
  48. data/lib/httpx/selector.rb +44 -20
  49. data/lib/httpx/session.rb +23 -33
  50. data/lib/httpx/transcoder/body.rb +1 -1
  51. data/lib/httpx/transcoder/deflate.rb +13 -8
  52. data/lib/httpx/transcoder/json.rb +1 -1
  53. data/lib/httpx/transcoder/multipart/decoder.rb +4 -4
  54. data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
  55. data/lib/httpx/transcoder/multipart.rb +16 -8
  56. data/lib/httpx/transcoder/utils/body_reader.rb +1 -2
  57. data/lib/httpx/transcoder/utils/deflater.rb +1 -2
  58. data/lib/httpx/transcoder.rb +4 -6
  59. data/lib/httpx/version.rb +1 -1
  60. data/sig/altsvc.rbs +3 -0
  61. data/sig/chainable.rbs +3 -3
  62. data/sig/connection.rbs +13 -6
  63. data/sig/loggable.rbs +5 -1
  64. data/sig/options.rbs +6 -2
  65. data/sig/plugins/auth/digest.rbs +6 -0
  66. data/sig/plugins/auth.rbs +28 -4
  67. data/sig/plugins/basic_auth.rbs +3 -3
  68. data/sig/plugins/callbacks.rbs +3 -0
  69. data/sig/plugins/digest_auth.rbs +2 -4
  70. data/sig/plugins/fiber_concurrency.rbs +6 -0
  71. data/sig/plugins/ntlm_auth.rbs +2 -2
  72. data/sig/plugins/oauth.rbs +46 -15
  73. data/sig/plugins/rate_limiter.rbs +1 -1
  74. data/sig/plugins/response_cache/file_store.rbs +2 -0
  75. data/sig/plugins/response_cache.rbs +4 -0
  76. data/sig/plugins/retries.rbs +8 -2
  77. data/sig/plugins/stream.rbs +13 -3
  78. data/sig/plugins/stream_bidi.rbs +5 -7
  79. data/sig/pool.rbs +1 -1
  80. data/sig/resolver/https.rbs +7 -0
  81. data/sig/resolver/multi.rbs +2 -9
  82. data/sig/resolver/native.rbs +1 -1
  83. data/sig/resolver/resolver.rbs +9 -8
  84. data/sig/resolver/system.rbs +4 -2
  85. data/sig/resolver.rbs +12 -3
  86. data/sig/response.rbs +3 -0
  87. data/sig/selector.rbs +2 -0
  88. data/sig/session.rbs +8 -8
  89. data/sig/transcoder/multipart.rbs +4 -2
  90. data/sig/transcoder.rbs +5 -1
  91. metadata +5 -1
@@ -8,6 +8,8 @@ module HTTPX
8
8
  module Plugins
9
9
  module Authentication
10
10
  class Digest
11
+ Error = Class.new(Error)
12
+
11
13
  def initialize(user, password, hashed: false, **)
12
14
  @user = user
13
15
  @password = password
@@ -29,19 +31,53 @@ module HTTPX
29
31
  # discard first token, it's Digest
30
32
  auth_info = authenticate[/^(\w+) (.*)/, 2]
31
33
 
32
- params = auth_info.split(/ *, */)
33
- .to_h { |val| val.split("=", 2) }
34
- .transform_values { |v| v.delete("\"") }
34
+ raise_format_error unless auth_info
35
+
36
+ s = StringScanner.new(auth_info)
37
+
38
+ params = {}
39
+ until s.eos?
40
+ k = s.scan_until(/=/)
41
+ raise_format_error unless k&.end_with?("=")
42
+
43
+ if s.peek(1) == "\""
44
+ s.skip("\"")
45
+ v = s.scan_until(/"/)
46
+ raise_format_error unless v&.end_with?("\"")
47
+
48
+ v = v[0..-2]
49
+ s.skip_until(/,/)
50
+ else
51
+ v = s.scan_until(/,|$/)
52
+
53
+ if v&.end_with?(",")
54
+ v = v[0..-2]
55
+ else
56
+ raise_format_error unless s.eos?
57
+ end
58
+
59
+ v = v[0..-2] if v&.end_with?(",")
60
+ end
61
+ params[k[0..-2]] = v
62
+ s.skip(/\s/)
63
+ end
64
+
35
65
  nonce = params["nonce"]
36
66
  nc = next_nonce
37
67
 
38
68
  # verify qop
39
69
  qop = params["qop"]
40
70
 
71
+ if qop
72
+ # some servers send multiple values wrapped in parentheses (i.e. "(qauth,)")
73
+ qop = qop[/\(?([^)]+)\)?/, 1]
74
+ qop = qop.split(",").map { |s| s.delete_prefix("'").delete_suffix("'") }.delete_if(&:empty?).map.first
75
+ end
76
+
41
77
  if params["algorithm"] =~ /(.*?)(-sess)?$/
42
78
  alg = Regexp.last_match(1)
43
79
  algorithm = ::Digest.const_get(alg)
44
- raise DigestError, "unknown algorithm \"#{alg}\"" unless algorithm
80
+ raise Error, "unknown algorithm \"#{alg}\"" unless algorithm
45
81
 
46
82
  sess = Regexp.last_match(2)
47
83
  else
@@ -96,6 +132,10 @@ module HTTPX
96
132
  def next_nonce
97
133
  @nonce += 1
98
134
  end
135
+
136
+ def raise_format_error
137
+ raise Error, "unsupported digest header format"
138
+ end
99
139
  end
100
140
  end
101
141
  end
@@ -10,13 +10,96 @@ module HTTPX
10
10
  # https://gitlab.com/os85/httpx/wikis/Auth#auth
11
11
  #
12
12
  module Auth
13
+ def self.subplugins
14
+ {
15
+ retries: AuthRetries,
16
+ }
17
+ end
18
+
19
+ module OptionsMethods
20
+ def option_auth_header_value(value)
21
+ value
22
+ end
23
+
24
+ def option_auth_header_type(value)
25
+ value
26
+ end
27
+
28
+ def option_generate_auth_value_on_retry(value)
29
+ raise TypeError, "`:generate_auth_value_on_retry` must be a callable" unless value.respond_to?(:call)
30
+
31
+ value
32
+ end
33
+ end
34
+
13
35
  module InstanceMethods
14
- def authorization(token)
15
- with(headers: { "authorization" => token })
36
+ def initialize(*)
37
+ super
38
+
39
+ @auth_header_value = nil
40
+ @skip_auth_header_value = false
41
+ end
42
+
43
+ def authorization(token = nil, auth_header_type: nil, &blk)
44
+ with(auth_header_type: auth_header_type, auth_header_value: token || blk)
45
+ end
46
+
47
+ def bearer_auth(token = nil, &blk)
48
+ authorization(token, auth_header_type: "Bearer", &blk)
49
+ end
50
+
51
+ def skip_auth_header
52
+ @skip_auth_header_value = true
53
+ yield
54
+ ensure
55
+ @skip_auth_header_value = false
56
+ end
57
+
58
+ def reset_auth_header_value!
59
+ @auth_header_value = nil
60
+ end
61
+
62
+ private
63
+
64
+ def send_request(request, *)
65
+ return super if @skip_auth_header_value
66
+
67
+ @auth_header_value ||= generate_auth_token
68
+
69
+ request.authorize(@auth_header_value) if @auth_header_value
70
+
71
+ super
72
+ end
73
+
74
+ def generate_auth_token
75
+ return unless (auth_value = @options.auth_header_value)
76
+
77
+ auth_value = auth_value.call(self) if auth_value.respond_to?(:call)
78
+
79
+ auth_value
16
80
  end
81
+ end
82
+
83
+ module RequestMethods
84
+ def authorize(auth_value)
85
+ if (auth_type = @options.auth_header_type)
86
+ auth_value = "#{auth_type} #{auth_value}"
87
+ end
88
+
89
+ @headers.add("authorization", auth_value)
90
+ end
91
+ end
92
+
93
+ module AuthRetries
94
+ module InstanceMethods
95
+ def prepare_to_retry(request, response)
96
+ super
97
+
98
+ return unless @options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response)
17
99
 
18
- def bearer_auth(token)
19
- authorization("Bearer #{token}")
100
+ request.headers.get("authorization").pop
101
+ @auth_header_value = generate_auth_token
102
+ end
20
103
  end
21
104
  end
22
105
  end
@@ -94,7 +94,6 @@ module HTTPX
94
94
  region: AwsSdkAuthentication.region(@options.aws_profile),
95
95
  **options
96
96
  )
97
-
98
97
  aws_sigv4_authentication(
99
98
  credentials: credentials,
100
99
  region: region,
@@ -64,7 +64,7 @@ module HTTPX
64
64
 
65
65
  emit_or_callback_error(:connection_opened, connection.origin, connection.io.socket)
66
66
  end
67
- connection.on(:close) do
67
+ connection.on(:callback_connection_closed) do
68
68
  next unless connection.current_session == self
69
69
 
70
70
  emit_or_callback_error(:connection_closed, connection.origin) if connection.used?
@@ -121,6 +121,20 @@ module HTTPX
121
121
  raise e.cause
122
122
  end
123
123
  end
124
+
125
+ module ConnectionMethods
126
+ private
127
+
128
+ def disconnect
129
+ return if @exhausted
130
+
131
+ return unless @current_session && @current_selector
132
+
133
+ emit(:callback_connection_closed)
134
+
135
+ super
136
+ end
137
+ end
124
138
  end
125
139
  register_plugin :callbacks, Callbacks
126
140
  end
@@ -7,6 +7,7 @@ module HTTPX
7
7
  # Contains the single cookie info: name, value and attributes.
8
8
  class Cookie
9
9
  include Comparable
10
+
10
11
  # Maximum number of bytes per cookie (RFC 6265 6.1 requires 4096 at
11
12
  # least)
12
13
  MAX_LENGTH = 4096
@@ -8,15 +8,14 @@ module HTTPX
8
8
  # https://gitlab.com/os85/httpx/wikis/Auth#digest-auth
9
9
  #
10
10
  module DigestAuth
11
- DigestError = Class.new(Error)
12
-
13
11
  class << self
14
12
  def extra_options(options)
15
13
  options.merge(max_concurrent_requests: 1)
16
14
  end
17
15
 
18
- def load_dependencies(*)
16
+ def load_dependencies(klass)
19
17
  require_relative "auth/digest"
18
+ klass.plugin(:auth)
20
19
  end
21
20
  end
22
21
 
@@ -48,11 +47,11 @@ module HTTPX
48
47
 
49
48
  probe_response = wrap { super(request).first }
50
49
 
51
- return probe_response unless probe_response.is_a?(Response)
50
+ return [probe_response] * requests.size unless probe_response.is_a?(Response)
52
51
 
53
52
  if probe_response.status == 401 && digest.can_authenticate?(probe_response.headers["www-authenticate"])
54
53
  request.transition(:idle)
55
- request.headers["authorization"] = digest.authenticate(request, probe_response.headers["www-authenticate"])
54
+ request.authorize(digest.authenticate(request, probe_response.headers["www-authenticate"]))
56
55
  super(request)
57
56
  else
58
57
  probe_response
@@ -6,12 +6,13 @@ module HTTPX
6
6
  #
7
7
  # This enables integration with fiber scheduler implementations such as [async](https://github.com/async).
8
8
  #
9
- # # https://gitlab.com/os85/httpx/wikis/FiberConcurrency
9
+ # # https://gitlab.com/os85/httpx/wikis/Fiber-Concurrency
10
10
  #
11
11
  module FiberConcurrency
12
12
  def self.subplugins
13
13
  {
14
14
  h2c: FiberConcurrencyH2C,
15
+ stream: FiberConcurrencyStream,
15
16
  }
16
17
  end
17
18
 
@@ -188,6 +189,20 @@ module HTTPX
188
189
  end
189
190
  end
190
191
  end
192
+
193
+ module FiberConcurrencyStream
194
+ module StreamResponseMethods
195
+ def close
196
+ unless @request.current_context?
197
+ @request.close
198
+
199
+ return
200
+ end
201
+
202
+ super
203
+ end
204
+ end
205
+ end
191
206
  end
192
207
 
193
208
  register_plugin :fiber_concurrency, FiberConcurrency
@@ -48,7 +48,7 @@ module HTTPX
48
48
  until message.empty?
49
49
  compressed, size = message.unpack("CL>")
50
50
 
51
- encoded_data = message.byteslice(5..size + 5 - 1)
51
+ encoded_data = message.byteslice(5..(size + 5 - 1))
52
52
 
53
53
  if compressed == 1
54
54
  grpc_encodings.reverse_each do |encoding|
@@ -249,7 +249,7 @@ module HTTPX
249
249
  call
250
250
  end
251
251
 
252
- def build_grpc_request(rpc_method, input, deadline:, metadata: nil, **)
252
+ def build_grpc_request(rpc_method, input, deadline:, metadata: nil, **opts)
253
253
  uri = @options.origin.dup
254
254
  rpc_method = "/#{rpc_method}" unless rpc_method.start_with?("/")
255
255
  rpc_method = "/#{@options.grpc_service}#{rpc_method}" if @options.grpc_service
@@ -273,7 +273,7 @@ module HTTPX
273
273
 
274
274
  headers.merge!(@options.call_credentials.call.transform_keys(&:to_s)) if @options.call_credentials
275
275
 
276
- build_request("POST", uri, headers: headers, body: input)
276
+ build_request("POST", uri, headers: headers, body: input, **opts)
277
277
  end
278
278
  end
279
279
  end
@@ -45,7 +45,7 @@ module HTTPX
45
45
  debug_level: @options ? @options.debug_level : DEBUG_LEVEL,
46
46
  debug: nil
47
47
  ) do
48
- "[ELAPSED TIME]: #{label}: #{elapsed} (ms)" << "\e[0m"
48
+ "[ELAPSED TIME]: #{label}: #{elapsed} (ms)\e[0m"
49
49
  end
50
50
  end
51
51
  end
@@ -7,8 +7,9 @@ module HTTPX
7
7
  #
8
8
  module NTLMAuth
9
9
  class << self
10
- def load_dependencies(_klass)
10
+ def load_dependencies(klass)
11
11
  require_relative "auth/ntlm"
12
+ klass.plugin(:auth)
12
13
  end
13
14
 
14
15
  def extra_options(options)
@@ -38,14 +39,15 @@ module HTTPX
38
39
  ntlm = request.options.ntlm
39
40
 
40
41
  if ntlm
41
- request.headers["authorization"] = ntlm.negotiate
42
+ request.authorize(ntlm.negotiate)
42
43
  probe_response = wrap { super(request).first }
43
44
 
44
45
  return probe_response unless probe_response.is_a?(Response)
45
46
 
46
47
  if probe_response.status == 401 && ntlm.can_authenticate?(probe_response.headers["www-authenticate"])
47
48
  request.transition(:idle)
48
- request.headers["authorization"] = ntlm.authenticate(request, probe_response.headers["www-authenticate"])
49
+ request.headers.get("authorization").pop
50
+ request.authorize(ntlm.authenticate(request, probe_response.headers["www-authenticate"]).encode("utf-8"))
49
51
  super(request)
50
52
  else
51
53
  probe_response
@@ -2,21 +2,38 @@
2
2
 
3
3
  module HTTPX
4
4
  module Plugins
5
+ #
6
+ # This plugin adds support for managing an OAuth Session associated with the given session.
7
+ #
8
+ # The scope of OAuth support is limited to the `client_crendentials` and `refresh_token` grants.
5
9
  #
6
10
  # https://gitlab.com/os85/httpx/wikis/OAuth
7
11
  #
8
12
  module OAuth
9
13
  class << self
10
- def load_dependencies(_klass)
14
+ def load_dependencies(klass)
11
15
  require_relative "auth/basic"
16
+ klass.plugin(:auth)
17
+ end
18
+
19
+ def subplugins
20
+ {
21
+ retries: OAuthRetries,
22
+ }
23
+ end
24
+
25
+ def extra_options(options)
26
+ options.merge(auth_header_type: "Bearer")
12
27
  end
13
28
  end
14
29
 
15
30
  SUPPORTED_GRANT_TYPES = %w[client_credentials refresh_token].freeze
16
31
  SUPPORTED_AUTH_METHODS = %w[client_secret_basic client_secret_post].freeze
17
32
 
33
+ # Implements the bulk of functionality and maintains the state associated with the
34
+ # management of the the lifecycle of an OAuth session.
18
35
  class OAuthSession
19
- attr_reader :grant_type, :client_id, :client_secret, :access_token, :refresh_token, :scope, :audience
36
+ attr_reader :access_token, :refresh_token
20
37
 
21
38
  def initialize(
22
39
  issuer:,
@@ -27,7 +44,6 @@ module HTTPX
27
44
  scope: nil,
28
45
  audience: nil,
29
46
  token_endpoint: nil,
30
- response_type: nil,
31
47
  grant_type: nil,
32
48
  token_endpoint_auth_method: nil
33
49
  )
@@ -35,7 +51,6 @@ module HTTPX
35
51
  @client_id = client_id
36
52
  @client_secret = client_secret
37
53
  @token_endpoint = URI(token_endpoint) if token_endpoint
38
- @response_type = response_type
39
54
  @scope = case scope
40
55
  when String
41
56
  scope.split
@@ -47,6 +62,8 @@ module HTTPX
47
62
  @refresh_token = refresh_token
48
63
  @token_endpoint_auth_method = String(token_endpoint_auth_method) if token_endpoint_auth_method
49
64
  @grant_type = grant_type || (@refresh_token ? "refresh_token" : "client_credentials")
65
+ @access_token = access_token
66
+ @refresh_token = refresh_token
50
67
 
51
68
  unless @token_endpoint_auth_method.nil? || SUPPORTED_AUTH_METHODS.include?(@token_endpoint_auth_method)
52
69
  raise Error, "#{@token_endpoint_auth_method} is not a supported auth method"
@@ -57,28 +74,75 @@ module HTTPX
57
74
  raise Error, "#{@grant_type} is not a supported grant type"
58
75
  end
59
76
 
77
+ # returns the URL where to request access and refresh tokens from.
60
78
  def token_endpoint
61
79
  @token_endpoint || "#{@issuer}/token"
62
80
  end
63
81
 
82
+ # returns the oauth-documented authorization method to use when requesting a token.
64
83
  def token_endpoint_auth_method
65
84
  @token_endpoint_auth_method || "client_secret_basic"
66
85
  end
67
86
 
68
- def load(http)
69
- return if @grant_type && @scope
87
+ def reset!
88
+ @access_token = nil
89
+ end
70
90
 
71
- metadata = http.get("#{@issuer}/.well-known/oauth-authorization-server").raise_for_status.json
91
+ # when not available, it uses the +http+ object to request new access and refresh tokens.
92
+ def fetch_access_token(http)
93
+ return access_token if access_token
72
94
 
73
- @token_endpoint = metadata["token_endpoint"]
74
- @scope = metadata["scopes_supported"]
75
- @grant_type = Array(metadata["grant_types_supported"]).find { |gr| SUPPORTED_GRANT_TYPES.include?(gr) }
76
- @token_endpoint_auth_method = Array(metadata["token_endpoint_auth_methods_supported"]).find do |am|
77
- SUPPORTED_AUTH_METHODS.include?(am)
95
+ load(http)
96
+
97
+ # always prefer refresh token grant if a refresh token is available
98
+ grant_type = @refresh_token ? "refresh_token" : @grant_type
99
+
100
+ headers = {} # : Hash[String ,String]
101
+ form_post = {
102
+ "grant_type" => @grant_type,
103
+ "scope" => Array(@scope).join(" "),
104
+ "audience" => @audience,
105
+ }.compact
106
+
107
+ # auth
108
+ case token_endpoint_auth_method
109
+ when "client_secret_post"
110
+ form_post["client_id"] = @client_id
111
+ form_post["client_secret"] = @client_secret
112
+ when "client_secret_basic"
113
+ headers["authorization"] = Authentication::Basic.new(@client_id, @client_secret).authenticate
78
114
  end
79
- nil
115
+
116
+ case grant_type
117
+ when "client_credentials"
118
+ # do nothing
119
+ when "refresh_token"
120
+ raise Error, "cannot use the `\"refresh_token\"` grant type without a refresh token" unless refresh_token
121
+
122
+ form_post["refresh_token"] = refresh_token
123
+ end
124
+
125
+ # POST /token
126
+ token_request = http.build_request("POST", token_endpoint, headers: headers, form: form_post)
127
+
128
+ token_request.headers.delete("authorization") unless token_endpoint_auth_method == "client_secret_basic"
129
+
130
+ token_response = http.skip_auth_header { http.request(token_request) }
131
+
132
+ begin
133
+ token_response.raise_for_status
134
+ rescue HTTPError => e
135
+ @refresh_token = nil if e.response.status == 401 && (grant_type == "refresh_token")
136
+ raise e
137
+ end
138
+
139
+ payload = token_response.json
140
+
141
+ @refresh_token = payload["refresh_token"] || @refresh_token
142
+ @access_token = payload["access_token"]
80
143
  end
81
144
 
145
+ # TODO: remove this after deprecating the `:oauth_session` option
82
146
  def merge(other)
83
147
  obj = dup
84
148
 
@@ -97,12 +161,32 @@ module HTTPX
97
161
  end
98
162
  obj
99
163
  end
164
+
165
+ private
166
+
167
+ # uses +http+ to fetch for the oauth server metadata.
168
+ def load(http)
169
+ return if @grant_type && @scope
170
+
171
+ metadata = http.skip_auth_header { http.get("#{@issuer}/.well-known/oauth-authorization-server").raise_for_status.json }
172
+
173
+ @token_endpoint = metadata["token_endpoint"]
174
+ @scope = metadata["scopes_supported"]
175
+ @grant_type = Array(metadata["grant_types_supported"]).find { |gr| SUPPORTED_GRANT_TYPES.include?(gr) }
176
+ @token_endpoint_auth_method = Array(metadata["token_endpoint_auth_methods_supported"]).find do |am|
177
+ SUPPORTED_AUTH_METHODS.include?(am)
178
+ end
179
+ nil
180
+ end
100
181
  end
101
182
 
102
183
  module OptionsMethods
103
184
  private
104
185
 
105
186
  def option_oauth_session(value)
187
+ warn "DEPRECATION WARNING: option `:oauth_session` is deprecated. " \
188
+ "Use `:oauth_options` instead."
189
+
106
190
  case value
107
191
  when Hash
108
192
  OAuthSession.new(**value)
@@ -112,69 +196,91 @@ module HTTPX
112
196
  raise TypeError, ":oauth_session must be a #{OAuthSession}"
113
197
  end
114
198
  end
199
+
200
+ def option_oauth_options(value)
201
+ value = Hash[value] unless value.is_a?(Hash)
202
+ value
203
+ end
115
204
  end
116
205
 
117
206
  module InstanceMethods
118
- def oauth_auth(**args)
119
- with(oauth_session: OAuthSession.new(**args))
120
- end
207
+ attr_reader :oauth_session
208
+ protected :oauth_session
121
209
 
122
- def with_access_token
123
- oauth_session = @options.oauth_session
210
+ def initialize(*)
211
+ super
124
212
 
125
- oauth_session.load(self)
213
+ @oauth_session = if @options.oauth_options
214
+ OAuthSession.new(**@options.oauth_options)
215
+ elsif @options.oauth_session
216
+ @oauth_session = @options.oauth_session.dup
217
+ end
218
+ end
126
219
 
127
- grant_type = oauth_session.grant_type
220
+ def initialize_dup(other)
221
+ super
222
+ @oauth_session = other.instance_variable_get(:@oauth_session).dup
223
+ end
128
224
 
129
- headers = {}
130
- form_post = {
131
- "grant_type" => grant_type,
132
- "scope" => Array(oauth_session.scope).join(" "),
133
- "audience" => oauth_session.audience,
134
- }.compact
225
+ def oauth_auth(**args)
226
+ warn "DEPRECATION WARNING: `#{__method__}` is deprecated. " \
227
+ "Use `with(oauth_options: options)` instead."
135
228
 
136
- # auth
137
- case oauth_session.token_endpoint_auth_method
138
- when "client_secret_post"
139
- form_post["client_id"] = oauth_session.client_id
140
- form_post["client_secret"] = oauth_session.client_secret
141
- when "client_secret_basic"
142
- headers["authorization"] = Authentication::Basic.new(oauth_session.client_id, oauth_session.client_secret).authenticate
143
- end
229
+ with(oauth_options: args)
230
+ end
144
231
 
145
- case grant_type
146
- when "client_credentials"
147
- # do nothing
148
- when "refresh_token"
149
- form_post["refresh_token"] = oauth_session.refresh_token
150
- end
232
+ # will eagerly negotiate new oauth tokens with the issuer
233
+ def refresh_oauth_tokens!
234
+ return unless @oauth_session
235
+
236
+ @oauth_session.reset!
237
+ @oauth_session.fetch_access_token(self)
238
+ end
151
239
 
152
- token_request = build_request("POST", oauth_session.token_endpoint, headers: headers, form: form_post)
153
- token_request.headers.delete("authorization") unless oauth_session.token_endpoint_auth_method == "client_secret_basic"
240
+ # TODO: deprecate
241
+ def with_access_token
242
+ warn "DEPRECATION WARNING: `#{__method__}` is deprecated. " \
243
+ "The session will automatically handle token lifecycles for you."
154
244
 
155
- token_response = request(token_request)
156
- token_response.raise_for_status
245
+ other_session = dup # : instance
246
+ oauth_session = other_session.oauth_session
247
+ oauth_session.fetch_access_token(other_session)
248
+ other_session
249
+ end
157
250
 
158
- payload = token_response.json
251
+ private
159
252
 
160
- access_token = payload["access_token"]
161
- refresh_token = payload["refresh_token"]
253
+ def generate_auth_token
254
+ return unless @oauth_session
162
255
 
163
- with(oauth_session: oauth_session.merge(access_token: access_token, refresh_token: refresh_token))
256
+ @oauth_session.fetch_access_token(self)
164
257
  end
258
+ end
165
259
 
166
- def build_request(*)
167
- request = super
168
-
169
- return request if request.headers.key?("authorization")
260
+ module OAuthRetries
261
+ class << self
262
+ def extra_options(options)
263
+ options.merge(
264
+ retry_on: method(:response_oauth_error?),
265
+ generate_auth_value_on_retry: method(:response_oauth_error?)
266
+ )
267
+ end
170
268
 
171
- oauth_session = @options.oauth_session
269
+ def response_oauth_error?(res)
270
+ res.is_a?(Response) && res.status == 401
271
+ end
272
+ end
172
273
 
173
- return request unless oauth_session && oauth_session.access_token
274
+ module InstanceMethods
275
+ def prepare_to_retry(_request, response)
276
+ unless @oauth_session && @options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response)
277
+ return super
278
+ end
174
279
 
175
- request.headers["authorization"] = "Bearer #{oauth_session.access_token}"
280
+ @oauth_session.reset!
176
281
 
177
- request
282
+ super
283
+ end
178
284
  end
179
285
  end
180
286
  end