oauth2 1.4.9 → 2.0.17

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 (55) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +706 -88
  4. data/CITATION.cff +20 -0
  5. data/CODE_OF_CONDUCT.md +24 -23
  6. data/CONTRIBUTING.md +229 -0
  7. data/FUNDING.md +77 -0
  8. data/{LICENSE → LICENSE.txt} +2 -2
  9. data/OIDC.md +158 -0
  10. data/README.md +1513 -251
  11. data/REEK +0 -0
  12. data/RUBOCOP.md +71 -0
  13. data/SECURITY.md +21 -0
  14. data/lib/oauth2/access_token.rb +276 -39
  15. data/lib/oauth2/authenticator.rb +45 -8
  16. data/lib/oauth2/client.rb +406 -129
  17. data/lib/oauth2/error.rb +59 -24
  18. data/lib/oauth2/filtered_attributes.rb +52 -0
  19. data/lib/oauth2/response.rb +127 -36
  20. data/lib/oauth2/strategy/assertion.rb +68 -40
  21. data/lib/oauth2/strategy/auth_code.rb +25 -4
  22. data/lib/oauth2/strategy/client_credentials.rb +3 -3
  23. data/lib/oauth2/strategy/implicit.rb +17 -2
  24. data/lib/oauth2/strategy/password.rb +14 -4
  25. data/lib/oauth2/version.rb +1 -59
  26. data/lib/oauth2.rb +79 -12
  27. data/sig/oauth2/access_token.rbs +25 -0
  28. data/sig/oauth2/authenticator.rbs +22 -0
  29. data/sig/oauth2/client.rbs +52 -0
  30. data/sig/oauth2/error.rbs +8 -0
  31. data/sig/oauth2/filtered_attributes.rbs +6 -0
  32. data/sig/oauth2/response.rbs +18 -0
  33. data/sig/oauth2/strategy.rbs +34 -0
  34. data/sig/oauth2/version.rbs +5 -0
  35. data/sig/oauth2.rbs +9 -0
  36. data.tar.gz.sig +0 -0
  37. metadata +336 -89
  38. metadata.gz.sig +0 -0
  39. data/lib/oauth2/mac_token.rb +0 -130
  40. data/spec/fixtures/README.md +0 -11
  41. data/spec/fixtures/RS256/jwtRS256.key +0 -51
  42. data/spec/fixtures/RS256/jwtRS256.key.pub +0 -14
  43. data/spec/helper.rb +0 -33
  44. data/spec/oauth2/access_token_spec.rb +0 -218
  45. data/spec/oauth2/authenticator_spec.rb +0 -86
  46. data/spec/oauth2/client_spec.rb +0 -556
  47. data/spec/oauth2/mac_token_spec.rb +0 -122
  48. data/spec/oauth2/response_spec.rb +0 -96
  49. data/spec/oauth2/strategy/assertion_spec.rb +0 -113
  50. data/spec/oauth2/strategy/auth_code_spec.rb +0 -108
  51. data/spec/oauth2/strategy/base_spec.rb +0 -7
  52. data/spec/oauth2/strategy/client_credentials_spec.rb +0 -71
  53. data/spec/oauth2/strategy/implicit_spec.rb +0 -28
  54. data/spec/oauth2/strategy/password_spec.rb +0 -58
  55. data/spec/oauth2/version_spec.rb +0 -23
data/REEK ADDED
File without changes
data/RUBOCOP.md ADDED
@@ -0,0 +1,71 @@
1
+ # RuboCop Usage Guide
2
+
3
+ ## Overview
4
+
5
+ A tale of two RuboCop plugin gems.
6
+
7
+ ### RuboCop Gradual
8
+
9
+ This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
10
+
11
+ ### RuboCop LTS
12
+
13
+ This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
14
+ RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
15
+
16
+ ## Checking RuboCop Violations
17
+
18
+ To check for RuboCop violations in this project, always use:
19
+
20
+ ```bash
21
+ bundle exec rake rubocop_gradual:check
22
+ ```
23
+
24
+ **Do not use** the standard RuboCop commands like:
25
+ - `bundle exec rubocop`
26
+ - `rubocop`
27
+
28
+ ## Understanding the Lock File
29
+
30
+ The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
31
+
32
+ 1. Prevent new violations while gradually fixing existing ones
33
+ 2. Track progress on code style improvements
34
+ 3. Ensure CI builds don't fail due to pre-existing violations
35
+
36
+ ## Common Commands
37
+
38
+ - **Check violations**
39
+ - `bundle exec rake rubocop_gradual`
40
+ - `bundle exec rake rubocop_gradual:check`
41
+ - **(Safe) Autocorrect violations, and update lockfile if no new violations**
42
+ - `bundle exec rake rubocop_gradual:autocorrect`
43
+ - **Force update the lock file (w/o autocorrect) to match violations present in code**
44
+ - `bundle exec rake rubocop_gradual:force_update`
45
+
46
+ ## Workflow
47
+
48
+ 1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
49
+ a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
50
+ 2. If there are new violations, either:
51
+ - Fix them in your code
52
+ - Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
53
+ 3. Commit the updated `.rubocop_gradual.lock` file along with your changes
54
+
55
+ ## Never add inline RuboCop disables
56
+
57
+ Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
58
+
59
+ - Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
60
+ - Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
61
+ - `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
62
+ - If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
63
+
64
+ In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
65
+
66
+ ## Benefits of rubocop_gradual
67
+
68
+ - Allows incremental adoption of code style rules
69
+ - Prevents CI failures due to pre-existing violations
70
+ - Provides a clear record of code style debt
71
+ - Enables focused efforts on improving code quality over time
data/SECURITY.md ADDED
@@ -0,0 +1,21 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |----------|-----------|
7
+ | 1.latest | ✅ |
8
+
9
+ ## Security contact information
10
+
11
+ To report a security vulnerability, please use the
12
+ [Tidelift security contact](https://tidelift.com/security).
13
+ Tidelift will coordinate the fix and disclosure.
14
+
15
+ ## Additional Support
16
+
17
+ If you are interested in support for versions older than the latest release,
18
+ please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
19
+ or find other sponsorship links in the [README].
20
+
21
+ [README]: README.md
@@ -1,59 +1,182 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :nocov:
4
+ begin
5
+ # The first version of hashie that has a version file was 1.1.0
6
+ # The first version of hashie that required the version file at runtime was 3.2.0
7
+ # If it has already been loaded then this is very low cost, as Kernel.require uses maintains a cache
8
+ # If this it hasn't this will work to get it loaded, and then we will be able to use
9
+ # defined?(Hashie::Version)
10
+ # as a test.
11
+ # TODO: get rid this mess when we drop Hashie < 3.2, as Hashie will self-load its version then
12
+ require "hashie/version"
13
+ rescue LoadError
14
+ nil
15
+ end
16
+ # :nocov:
17
+
3
18
  module OAuth2
4
- class AccessToken
5
- attr_reader :client, :token, :expires_in, :expires_at, :params
6
- attr_accessor :options, :refresh_token
19
+ class AccessToken # rubocop:disable Metrics/ClassLength
20
+ TOKEN_KEYS_STR = %w[access_token id_token token accessToken idToken].freeze
21
+ TOKEN_KEYS_SYM = %i[access_token id_token token accessToken idToken].freeze
22
+ TOKEN_KEY_LOOKUP = TOKEN_KEYS_STR + TOKEN_KEYS_SYM
23
+
24
+ include FilteredAttributes
25
+
26
+ attr_reader :client, :token, :expires_in, :expires_at, :expires_latency, :params
27
+ attr_accessor :options, :refresh_token, :response
28
+ filtered_attributes :token, :refresh_token
7
29
 
8
- # Should these methods be deprecated?
9
30
  class << self
10
31
  # Initializes an AccessToken from a Hash
11
32
  #
12
- # @param [Client] the OAuth2::Client instance
13
- # @param [Hash] a hash of AccessToken property values
14
- # @return [AccessToken] the initalized AccessToken
33
+ # @param [OAuth2::Client] client the OAuth2::Client instance
34
+ # @param [Hash] hash a hash containing the token and other properties
35
+ # @option hash [String] 'access_token' the access token value
36
+ # @option hash [String] 'id_token' alternative key for the access token value
37
+ # @option hash [String] 'token' alternative key for the access token value
38
+ # @option hash [String] 'refresh_token' (optional) the refresh token value
39
+ # @option hash [Integer, String] 'expires_in' (optional) number of seconds until token expires
40
+ # @option hash [Integer, String] 'expires_at' (optional) epoch time in seconds when token expires
41
+ # @option hash [Integer, String] 'expires_latency' (optional) seconds to reduce token validity by
42
+ #
43
+ # @return [OAuth2::AccessToken] the initialized AccessToken
44
+ #
45
+ # @note The method will use the first found token key in the following order:
46
+ # 'access_token', 'id_token', 'token' (or their symbolic versions)
47
+ # @note If multiple token keys are present, a warning will be issued unless
48
+ # OAuth2.config.silence_extra_tokens_warning is true
49
+ # @note If no token keys are present, a warning will be issued unless
50
+ # OAuth2.config.silence_no_tokens_warning is true
51
+ # @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option.
52
+ # @note If snaky key conversion is being used, token_name needs to match the converted key.
53
+ #
54
+ # @example
55
+ # hash = { 'access_token' => 'token_value', 'refresh_token' => 'refresh_value' }
56
+ # access_token = OAuth2::AccessToken.from_hash(client, hash)
15
57
  def from_hash(client, hash)
16
- hash = hash.dup
17
- new(client, hash.delete('access_token') || hash.delete(:access_token), hash)
58
+ fresh = hash.dup
59
+ # If token_name is present, then use that key name
60
+ key =
61
+ if fresh.key?(:token_name)
62
+ t_key = fresh[:token_name]
63
+ no_tokens_warning(fresh, t_key)
64
+ t_key
65
+ else
66
+ # Otherwise, if one of the supported default keys is present, use whichever has precedence
67
+ supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
68
+ t_key = supported_keys[0]
69
+ extra_tokens_warning(supported_keys, t_key)
70
+ t_key
71
+ end
72
+ # :nocov:
73
+ # TODO: Get rid of this branching logic when dropping Hashie < v3.2
74
+ token = if !defined?(Hashie::VERSION) # i.e. <= "1.1.0"; the first Hashie to ship with a VERSION constant
75
+ warn("snaky_hash and oauth2 will drop support for Hashie v0 in the next major version. Please upgrade to a modern Hashie.")
76
+ # There is a bug in Hashie v0, which is accounts for.
77
+ fresh.delete(key) || fresh[key] || ""
78
+ else
79
+ fresh.delete(key) || ""
80
+ end
81
+ # :nocov:
82
+ new(client, token, fresh)
18
83
  end
19
84
 
20
85
  # Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
21
86
  #
22
87
  # @param [Client] client the OAuth2::Client instance
23
88
  # @param [String] kvform the application/x-www-form-urlencoded string
24
- # @return [AccessToken] the initalized AccessToken
89
+ # @return [AccessToken] the initialized AccessToken
25
90
  def from_kvform(client, kvform)
26
91
  from_hash(client, Rack::Utils.parse_query(kvform))
27
92
  end
93
+
94
+ private
95
+
96
+ # Having too many is sus, and may lead to bugs. Having none is fine (e.g. refresh flow doesn't need a token).
97
+ def extra_tokens_warning(supported_keys, key)
98
+ return if OAuth2.config.silence_extra_tokens_warning
99
+ return if supported_keys.length <= 1
100
+
101
+ warn("OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (#{supported_keys}); using #{key.inspect}.")
102
+ end
103
+
104
+ def no_tokens_warning(hash, key)
105
+ return if OAuth2.config.silence_no_tokens_warning
106
+ return if key && hash.key?(key)
107
+
108
+ warn(%[
109
+ OAuth2::AccessToken#from_hash key mismatch.
110
+ Custom token_name (#{key}) is not found in (#{hash.keys})
111
+ You may need to set `snaky: false`. See inline documentation for more info.
112
+ ])
113
+ end
28
114
  end
29
115
 
30
- # Initalize an AccessToken
116
+ # Initialize an AccessToken
117
+ #
118
+ # @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option.
119
+ # @note If no token is provided, the AccessToken will be considered invalid.
120
+ # This is to prevent the possibility of a token being accidentally
121
+ # created with no token value.
122
+ # If you want to create an AccessToken with no token value,
123
+ # you can pass in an empty string or nil for the token value.
124
+ # If you want to create an AccessToken with no token value and
125
+ # no refresh token, you can pass in an empty string or nil for the
126
+ # token value and nil for the refresh token, and `raise_errors: false`.
31
127
  #
32
128
  # @param [Client] client the OAuth2::Client instance
33
- # @param [String] token the Access Token value
129
+ # @param [String] token the Access Token value (optional, may not be used in refresh flows)
34
130
  # @param [Hash] opts the options to create the Access Token with
35
131
  # @option opts [String] :refresh_token (nil) the refresh_token value
36
132
  # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
37
133
  # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
38
- # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
39
- # one of :header, :body or :query
134
+ # @option opts [FixNum, String] :expires_latency (nil) the number of seconds by which AccessToken validity will be reduced to offset latency, @version 2.0+
135
+ # @option opts [Symbol, Hash, or callable] :mode (:header) the transmission mode of the Access Token parameter value:
136
+ # either one of :header, :body or :query; or a Hash with verb symbols as keys mapping to one of these symbols
137
+ # (e.g., {get: :query, post: :header, delete: :header}); or a callable that accepts a request-verb parameter
138
+ # and returns one of these three symbols.
40
139
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
140
+ #
141
+ # @example Verb-dependent Hash mode
142
+ # # Send token in query for GET, in header for POST/DELETE, in body for PUT/PATCH
143
+ # OAuth2::AccessToken.new(client, token, mode: {get: :query, post: :header, delete: :header, put: :body, patch: :body})
41
144
  # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
42
145
  # Access Token value in :body or :query transmission mode
146
+ # @option opts [String] :token_name (nil) the name of the response parameter that identifies the access token
147
+ # When nil one of TOKEN_KEY_LOOKUP will be used
43
148
  def initialize(client, token, opts = {})
44
149
  @client = client
45
150
  @token = token.to_s
46
151
  opts = opts.dup
47
- [:refresh_token, :expires_in, :expires_at].each do |arg|
152
+ %i[refresh_token expires_in expires_at expires_latency].each do |arg|
48
153
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
49
154
  end
50
- @expires_in ||= opts.delete('expires')
155
+ no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
156
+ if no_tokens
157
+ if @client.options[:raise_errors]
158
+ raise Error.new({
159
+ error: "OAuth2::AccessToken has no token",
160
+ error_description: "Options are: #{opts.inspect}",
161
+ })
162
+ elsif !OAuth2.config.silence_no_tokens_warning
163
+ warn("OAuth2::AccessToken has no token")
164
+ end
165
+ end
166
+ # @option opts [Fixnum, String] :expires is deprecated
167
+ @expires_in ||= opts.delete("expires")
51
168
  @expires_in &&= @expires_in.to_i
52
169
  @expires_at &&= convert_expires_at(@expires_at)
53
- @expires_at ||= Time.now.to_i + @expires_in if @expires_in
54
- @options = {:mode => opts.delete(:mode) || :header,
55
- :header_format => opts.delete(:header_format) || 'Bearer %s',
56
- :param_name => opts.delete(:param_name) || 'access_token'}
170
+ @expires_latency &&= @expires_latency.to_i
171
+ @expires_at ||= Time.now.to_i + @expires_in if @expires_in && !@expires_in.zero?
172
+ @expires_at -= @expires_latency if @expires_latency
173
+ @options = {
174
+ mode: opts.delete(:mode) || :header,
175
+ header_format: opts.delete(:header_format) || "Bearer %s",
176
+ param_name: opts.delete(:param_name) || "access_token",
177
+ }
178
+ @options[:token_name] = opts.delete(:token_name) if opts.key?(:token_name)
179
+
57
180
  @params = opts
58
181
  end
59
182
 
@@ -64,40 +187,131 @@ module OAuth2
64
187
  @params[key]
65
188
  end
66
189
 
67
- # Whether or not the token expires
190
+ # Whether the token expires
68
191
  #
69
192
  # @return [Boolean]
70
193
  def expires?
71
194
  !!@expires_at
72
195
  end
73
196
 
74
- # Whether or not the token is expired
197
+ # Check if token is expired
75
198
  #
76
- # @return [Boolean]
199
+ # @return [Boolean] true if the token is expired, false otherwise
77
200
  def expired?
78
- expires? && (expires_at < Time.now.to_i)
201
+ expires? && (expires_at <= Time.now.to_i)
79
202
  end
80
203
 
81
204
  # Refreshes the current Access Token
82
205
  #
83
- # @return [AccessToken] a new AccessToken
84
- # @note options should be carried over to the new AccessToken
85
- def refresh!(params = {})
86
- raise('A refresh_token is not available') unless refresh_token
206
+ # @param [Hash] params additional params to pass to the refresh token request
207
+ # @param [Hash] access_token_opts options that will be passed to the AccessToken initialization
208
+ #
209
+ # @yield [opts] The block to modify the refresh token request options
210
+ # @yieldparam [Hash] opts The options hash that can be modified
211
+ #
212
+ # @return [OAuth2::AccessToken] a new AccessToken instance
213
+ #
214
+ # @note current token's options are carried over to the new AccessToken
215
+ def refresh(params = {}, access_token_opts = {}, &block)
216
+ raise OAuth2::Error.new({error: "A refresh_token is not available"}) unless refresh_token
87
217
 
88
- params[:grant_type] = 'refresh_token'
218
+ params[:grant_type] = "refresh_token"
89
219
  params[:refresh_token] = refresh_token
90
- new_token = @client.get_token(params)
220
+ new_token = @client.get_token(params, access_token_opts, &block)
91
221
  new_token.options = options
92
- new_token.refresh_token = refresh_token unless new_token.refresh_token
222
+ if new_token.refresh_token
223
+ # Keep it if there is one
224
+ else
225
+ new_token.refresh_token = refresh_token
226
+ end
93
227
  new_token
94
228
  end
229
+ # A compatibility alias
230
+ # @note does not modify the receiver, so bang is not the default method
231
+ alias_method :refresh!, :refresh
232
+
233
+ # Revokes the token at the authorization server
234
+ #
235
+ # @param [Hash] params additional parameters to be sent during revocation
236
+ # @option params [String, Symbol, nil] :token_type_hint ('access_token' or 'refresh_token') hint about which token to revoke
237
+ # @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method]
238
+ #
239
+ # @yield [req] The block is passed the request being made, allowing customization
240
+ # @yieldparam [Faraday::Request] req The request object that can be modified
241
+ #
242
+ # @return [OAuth2::Response] OAuth2::Response instance
243
+ #
244
+ # @api public
245
+ #
246
+ # @raise [OAuth2::Error] if token_type_hint is invalid or the specified token is not available
247
+ #
248
+ # @note If the token passed to the request
249
+ # is an access token, the server MAY revoke the respective refresh
250
+ # token as well.
251
+ # @note If the token passed to the request
252
+ # is a refresh token and the authorization server supports the
253
+ # revocation of access tokens, then the authorization server SHOULD
254
+ # also invalidate all access tokens based on the same authorization
255
+ # grant
256
+ # @note If the server responds with HTTP status code 503, your code must
257
+ # assume the token still exists and may retry after a reasonable delay.
258
+ # The server may include a "Retry-After" header in the response to
259
+ # indicate how long the service is expected to be unavailable to the
260
+ # requesting client.
261
+ #
262
+ # @see https://datatracker.ietf.org/doc/html/rfc7009
263
+ # @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
264
+ def revoke(params = {}, &block)
265
+ token_type_hint_orig = params.delete(:token_type_hint)
266
+ token_type_hint = nil
267
+ revoke_token = case token_type_hint_orig
268
+ when "access_token", :access_token
269
+ token_type_hint = "access_token"
270
+ token
271
+ when "refresh_token", :refresh_token
272
+ token_type_hint = "refresh_token"
273
+ refresh_token
274
+ when nil
275
+ if token
276
+ token_type_hint = "access_token"
277
+ token
278
+ elsif refresh_token
279
+ token_type_hint = "refresh_token"
280
+ refresh_token
281
+ end
282
+ else
283
+ raise OAuth2::Error.new({error: "token_type_hint must be one of [nil, :refresh_token, :access_token], so if you need something else consider using a subclass or entirely custom AccessToken class."})
284
+ end
285
+ raise OAuth2::Error.new({error: "#{token_type_hint || "unknown token type"} is not available for revoking"}) unless revoke_token && !revoke_token.empty?
286
+
287
+ @client.revoke_token(revoke_token, token_type_hint, params, &block)
288
+ end
289
+ # A compatibility alias
290
+ # @note does not modify the receiver, so bang is not the default method
291
+ alias_method :revoke!, :revoke
95
292
 
96
293
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
97
294
  #
295
+ # @note Don't return expires_latency because it has already been deducted from expires_at
296
+ #
98
297
  # @return [Hash] a hash of AccessToken property values
99
298
  def to_hash
100
- params.merge(:access_token => token, :refresh_token => refresh_token, :expires_at => expires_at)
299
+ hsh = {
300
+ access_token: token,
301
+ refresh_token: refresh_token,
302
+ expires_at: expires_at,
303
+ mode: options[:mode],
304
+ header_format: options[:header_format],
305
+ param_name: options[:param_name],
306
+ }
307
+ hsh[:token_name] = options[:token_name] if options.key?(:token_name)
308
+ # TODO: Switch when dropping Ruby < 2.5 support
309
+ # params.transform_keys(&:to_sym) # Ruby 2.5 only
310
+ # Old Ruby transform_keys alternative:
311
+ sheesh = @params.each_with_object({}) { |(k, v), memo|
312
+ memo[k.to_sym] = v
313
+ }
314
+ sheesh.merge(hsh)
101
315
  end
102
316
 
103
317
  # Make a request with the Access Token
@@ -105,9 +319,18 @@ module OAuth2
105
319
  # @param [Symbol] verb the HTTP request method
106
320
  # @param [String] path the HTTP URL path of the request
107
321
  # @param [Hash] opts the options to make the request with
108
- # @see Client#request
322
+ # @option opts [Hash] :params additional URL parameters
323
+ # @option opts [Hash, String] :body the request body
324
+ # @option opts [Hash] :headers request headers
325
+ #
326
+ # @yield [req] The block to modify the request
327
+ # @yieldparam [Faraday::Request] req The request object that can be modified
328
+ #
329
+ # @return [OAuth2::Response] the response from the request
330
+ #
331
+ # @see OAuth2::Client#request
109
332
  def request(verb, path, opts = {}, &block)
110
- configure_authentication!(opts)
333
+ configure_authentication!(opts, verb)
111
334
  @client.request(verb, path, opts, &block)
112
335
  end
113
336
 
@@ -148,17 +371,31 @@ module OAuth2
148
371
 
149
372
  # Get the headers hash (includes Authorization token)
150
373
  def headers
151
- {'Authorization' => options[:header_format] % token}
374
+ {"Authorization" => options[:header_format] % token}
152
375
  end
153
376
 
154
377
  private
155
378
 
156
- def configure_authentication!(opts)
157
- case options[:mode]
379
+ def configure_authentication!(opts, verb)
380
+ mode_opt = options[:mode]
381
+ mode =
382
+ if mode_opt.respond_to?(:call)
383
+ mode_opt.call(verb)
384
+ elsif mode_opt.is_a?(Hash)
385
+ key = verb.to_sym
386
+ # Try symbol key first, then string key; default to :header when missing
387
+ mode_opt[key] || mode_opt[key.to_s] || :header
388
+ else
389
+ mode_opt
390
+ end
391
+
392
+ case mode
158
393
  when :header
159
394
  opts[:headers] ||= {}
160
395
  opts[:headers].merge!(headers)
161
396
  when :query
397
+ # OAuth 2.1 note: Bearer tokens in the query string are omitted from the spec due to security risks.
398
+ # Prefer the default :header mode whenever possible.
162
399
  opts[:params] ||= {}
163
400
  opts[:params][options[:param_name]] = token
164
401
  when :body
@@ -166,11 +403,11 @@ module OAuth2
166
403
  if opts[:body].is_a?(Hash)
167
404
  opts[:body][options[:param_name]] = token
168
405
  else
169
- opts[:body] << "&#{options[:param_name]}=#{token}"
406
+ opts[:body] += "&#{options[:param_name]}=#{token}"
170
407
  end
171
408
  # @todo support for multi-part (file uploads)
172
409
  else
173
- raise("invalid :mode option of #{options[:mode]}")
410
+ raise("invalid :mode option of #{mode}")
174
411
  end
175
412
  end
176
413
 
@@ -1,11 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'base64'
3
+ require "base64"
4
4
 
5
5
  module OAuth2
6
+ # Builds and applies client authentication to token and revoke requests.
7
+ #
8
+ # Depending on the selected mode, credentials are applied as Basic Auth
9
+ # headers, request body parameters, or only the client_id is sent (TLS).
6
10
  class Authenticator
11
+ include FilteredAttributes
12
+
13
+ # @return [Symbol, String] Authentication mode (e.g., :basic_auth, :request_body, :tls_client_auth, :private_key_jwt)
14
+ # @return [String, nil] Client identifier
15
+ # @return [String, nil] Client secret (filtered in inspected output)
7
16
  attr_reader :mode, :id, :secret
17
+ filtered_attributes :secret
8
18
 
19
+ # Create a new Authenticator
20
+ #
21
+ # @param [String, nil] id Client identifier
22
+ # @param [String, nil] secret Client secret
23
+ # @param [Symbol, String] mode Authentication mode
9
24
  def initialize(id, secret, mode)
10
25
  @id = id
11
26
  @secret = secret
@@ -14,7 +29,7 @@ module OAuth2
14
29
 
15
30
  # Apply the request credentials used to authenticate to the Authorization Server
16
31
  #
17
- # Depending on configuration, this might be as request params or as an
32
+ # Depending on the configuration, this might be as request params or as an
18
33
  # Authorization header.
19
34
  #
20
35
  # User-provided params and header take precedence.
@@ -36,35 +51,57 @@ module OAuth2
36
51
  end
37
52
  end
38
53
 
54
+ # Encodes a Basic Authorization header value for the provided credentials.
55
+ #
56
+ # @param [String] user The client identifier
57
+ # @param [String] password The client secret
58
+ # @return [String] The value to use for the Authorization header
39
59
  def self.encode_basic_auth(user, password)
40
- 'Basic ' + Base64.encode64(user + ':' + password).delete("\n")
60
+ "Basic #{Base64.strict_encode64("#{user}:#{password}")}"
41
61
  end
42
62
 
43
63
  private
44
64
 
45
65
  # Adds client_id and client_secret request parameters if they are not
46
66
  # already set.
67
+ #
68
+ # @param [Hash] params Request parameters
69
+ # @return [Hash] Updated parameters including client_id and client_secret
47
70
  def apply_params_auth(params)
48
- {'client_id' => id, 'client_secret' => secret}.merge(params)
71
+ result = {}
72
+ result["client_id"] = id unless id.nil?
73
+ result["client_secret"] = secret unless secret.nil?
74
+ result.merge(params)
49
75
  end
50
76
 
51
- # When using schemes that don't require the client_secret to be passed i.e TLS Client Auth,
77
+ # When using schemes that don't require the client_secret to be passed (e.g., TLS Client Auth),
52
78
  # we don't want to send the secret
79
+ #
80
+ # @param [Hash] params Request parameters
81
+ # @return [Hash] Updated parameters including only client_id
53
82
  def apply_client_id(params)
54
- {'client_id' => id}.merge(params)
83
+ result = {}
84
+ result["client_id"] = id unless id.nil?
85
+ result.merge(params)
55
86
  end
56
87
 
57
88
  # Adds an `Authorization` header with Basic Auth credentials if and only if
58
89
  # it is not already set in the params.
90
+ #
91
+ # @param [Hash] params Request parameters (may include :headers)
92
+ # @return [Hash] Updated parameters with Authorization header
59
93
  def apply_basic_auth(params)
60
94
  headers = params.fetch(:headers, {})
61
95
  headers = basic_auth_header.merge(headers)
62
- params.merge(:headers => headers)
96
+ params.merge(headers: headers)
63
97
  end
64
98
 
99
+ # Build the Basic Authorization header.
100
+ #
65
101
  # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
102
+ # @return [Hash] Header hash containing the Authorization entry
66
103
  def basic_auth_header
67
- {'Authorization' => self.class.encode_basic_auth(id, secret)}
104
+ {"Authorization" => self.class.encode_basic_auth(id, secret)}
68
105
  end
69
106
  end
70
107
  end