oauth2 2.0.9 → 2.0.25

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.
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 CHANGED
@@ -2,25 +2,20 @@
2
2
 
3
3
  ## Supported Versions
4
4
 
5
- | Version | Supported | EOL | Post-EOL / Enterprise |
6
- |----------|-----------|---------|---------------------------------------|
7
- | 2.latest | ✅ | 04/2024 | [Tidelift Subscription][tidelift-ref] |
8
- | 1.latest | ✅ | 04/2023 | [Tidelift Subscription][tidelift-ref] |
9
- | <= 1 | ⛔ | ⛔ | ⛔ |
5
+ | Version | Supported |
6
+ |----------|-----------|
7
+ | 2.0.latest | ✅ |
10
8
 
11
- ### EOL Policy
9
+ ## Security contact information
12
10
 
13
- Non-commercial support for the oldest version of Ruby (which itself is going EOL) will be dropped each year in April.
14
-
15
- ## Reporting a Vulnerability
16
-
17
- To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
11
+ To report a security vulnerability, please use the
12
+ [Tidelift security contact](https://tidelift.com/security).
18
13
  Tidelift will coordinate the fix and disclosure.
19
14
 
20
- ## OAuth2 for Enterprise
21
-
22
- Available as part of the Tidelift Subscription.
15
+ ## Additional Support
23
16
 
24
- The maintainers of oauth2 and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. [Learn more.][tidelift-ref]
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].
25
20
 
26
- [tidelift-ref]: https://tidelift.com/subscription/pkg/rubygems-oauth2?utm_source=rubygems-oauth2&utm_medium=referral&utm_campaign=enterprise&utm_term=repo
21
+ [README]: README.md
data/certs/pboling.pem ADDED
@@ -0,0 +1,27 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIEgDCCAuigAwIBAgIBATANBgkqhkiG9w0BAQsFADBDMRUwEwYDVQQDDAxwZXRl
3
+ ci5ib2xpbmcxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkW
4
+ A2NvbTAeFw0yNTA1MDQxNTMzMDlaFw00NTA0MjkxNTMzMDlaMEMxFTATBgNVBAMM
5
+ DHBldGVyLmJvbGluZzEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
6
+ LGQBGRYDY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAruUoo0WA
7
+ uoNuq6puKWYeRYiZekz/nsDeK5x/0IEirzcCEvaHr3Bmz7rjo1I6On3gGKmiZs61
8
+ LRmQ3oxy77ydmkGTXBjruJB+pQEn7UfLSgQ0xa1/X3kdBZt6RmabFlBxnHkoaGY5
9
+ mZuZ5+Z7walmv6sFD9ajhzj+oIgwWfnEHkXYTR8I6VLN7MRRKGMPoZ/yvOmxb2DN
10
+ coEEHWKO9CvgYpW7asIihl/9GMpKiRkcYPm9dGQzZc6uTwom1COfW0+ZOFrDVBuV
11
+ FMQRPswZcY4Wlq0uEBLPU7hxnCL9nKK6Y9IhdDcz1mY6HZ91WImNslOSI0S8hRpj
12
+ yGOWxQIhBT3fqCBlRIqFQBudrnD9jSNpSGsFvbEijd5ns7Z9ZMehXkXDycpGAUj1
13
+ to/5cuTWWw1JqUWrKJYoifnVhtE1o1DZ+LkPtWxHtz5kjDG/zR3MG0Ula0UOavlD
14
+ qbnbcXPBnwXtTFeZ3C+yrWpE4pGnl3yGkZj9SMTlo9qnTMiPmuWKQDatAgMBAAGj
15
+ fzB9MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBQE8uWvNbPVNRXZ
16
+ HlgPbc2PCzC4bjAhBgNVHREEGjAYgRZwZXRlci5ib2xpbmdAZ21haWwuY29tMCEG
17
+ A1UdEgQaMBiBFnBldGVyLmJvbGluZ0BnbWFpbC5jb20wDQYJKoZIhvcNAQELBQAD
18
+ ggGBAJbnUwfJQFPkBgH9cL7hoBfRtmWiCvdqdjeTmi04u8zVNCUox0A4gT982DE9
19
+ wmuN12LpdajxZONqbXuzZvc+nb0StFwmFYZG6iDwaf4BPywm2e/Vmq0YG45vZXGR
20
+ L8yMDSK1cQXjmA+ZBKOHKWavxP6Vp7lWvjAhz8RFwqF9GuNIdhv9NpnCAWcMZtpm
21
+ GUPyIWw/Cw/2wZp74QzZj6Npx+LdXoLTF1HMSJXZ7/pkxLCsB8m4EFVdb/IrW/0k
22
+ kNSfjtAfBHO8nLGuqQZVH9IBD1i9K6aSs7pT6TW8itXUIlkIUI2tg5YzW6OFfPzq
23
+ QekSkX3lZfY+HTSp/o+YvKkqWLUV7PQ7xh1ZYDtocpaHwgxe/j3bBqHE+CUPH2vA
24
+ 0V/FwdTRWcwsjVoOJTrYcff8pBZ8r2MvtAc54xfnnhGFzeRHfcltobgFxkAXdE6p
25
+ DVjBtqT23eugOqQ73umLcYDZkc36vnqGxUBSsXrzY9pzV5gGr2I8YUxMqf6ATrZt
26
+ L9nRqA==
27
+ -----END CERTIFICATE-----
@@ -1,27 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # simplecov:disable
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
+ # simplecov:enable
17
+
3
18
  module OAuth2
4
19
  class AccessToken # rubocop:disable Metrics/ClassLength
5
20
  TOKEN_KEYS_STR = %w[access_token id_token token accessToken idToken].freeze
6
21
  TOKEN_KEYS_SYM = %i[access_token id_token token accessToken idToken].freeze
7
22
  TOKEN_KEY_LOOKUP = TOKEN_KEYS_STR + TOKEN_KEYS_SYM
8
23
 
24
+ include FilteredAttributes
25
+
9
26
  attr_reader :client, :token, :expires_in, :expires_at, :expires_latency, :params
10
27
  attr_accessor :options, :refresh_token, :response
28
+ filtered_attributes :token, :refresh_token
11
29
 
12
30
  class << self
13
31
  # Initializes an AccessToken from a Hash
14
32
  #
15
- # @param [Client] client the OAuth2::Client instance
16
- # @param [Hash] hash a hash of AccessToken property values
17
- # @option hash [String, Symbol] 'access_token', 'id_token', 'token', :access_token, :id_token, or :token the access token
18
- # @return [AccessToken] the initialized 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)
19
57
  def from_hash(client, hash)
20
58
  fresh = hash.dup
21
- supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
22
- key = supported_keys[0]
23
- extra_tokens_warning(supported_keys, key)
24
- token = fresh.delete(key)
59
+ # If token_name is present, then use that key name
60
+ if fresh.key?(:token_name)
61
+ t_key = fresh[:token_name]
62
+ no_tokens_warning(fresh, t_key)
63
+ else
64
+ # Otherwise, if one of the supported default keys is present, use whichever has precedence
65
+ supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
66
+ t_key = supported_keys[0]
67
+ extra_tokens_warning(supported_keys, t_key)
68
+ end
69
+ # simplecov:disable
70
+ # TODO: Get rid of this branching logic when dropping Hashie < v3.2
71
+ token = extract_token_value(fresh, t_key)
72
+ # simplecov:enable
25
73
  new(client, token, fresh)
26
74
  end
27
75
 
@@ -43,10 +91,42 @@ module OAuth2
43
91
 
44
92
  warn("OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (#{supported_keys}); using #{key.inspect}.")
45
93
  end
94
+
95
+ def no_tokens_warning(hash, key)
96
+ return if OAuth2.config.silence_no_tokens_warning
97
+ return if key && hash.key?(key)
98
+
99
+ warn(%[
100
+ OAuth2::AccessToken#from_hash key mismatch.
101
+ Custom token_name (#{key}) is not found in (#{hash.keys})
102
+ You may need to set `snaky: false`. See inline documentation for more info.
103
+ ])
104
+ end
105
+
106
+ # simplecov:disable
107
+ def extract_token_value(fresh, key)
108
+ token_value = fresh.delete(key)
109
+ return token_value || "" if defined?(Hashie::VERSION)
110
+
111
+ warn("snaky_hash and oauth2 will drop support for Hashie v0 in the next major version. Please upgrade to a modern Hashie.")
112
+ # There is a bug in Hashie v0, which this accounts for.
113
+ token_value || fresh[key] || ""
114
+ end
115
+ # simplecov:enable
46
116
  end
47
117
 
48
118
  # Initialize an AccessToken
49
119
  #
120
+ # @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option.
121
+ # @note If no token is provided, the AccessToken will be considered invalid.
122
+ # This is to prevent the possibility of a token being accidentally
123
+ # created with no token value.
124
+ # If you want to create an AccessToken with no token value,
125
+ # you can pass in an empty string or nil for the token value.
126
+ # If you want to create an AccessToken with no token value and
127
+ # no refresh token, you can pass in an empty string or nil for the
128
+ # token value and nil for the refresh token, and `raise_errors: false`.
129
+ #
50
130
  # @param [Client] client the OAuth2::Client instance
51
131
  # @param [String] token the Access Token value (optional, may not be used in refresh flows)
52
132
  # @param [Hash] opts the options to create the Access Token with
@@ -54,15 +134,22 @@ module OAuth2
54
134
  # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
55
135
  # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
56
136
  # @option opts [FixNum, String] :expires_latency (nil) the number of seconds by which AccessToken validity will be reduced to offset latency, @version 2.0+
57
- # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
58
- # one of :header, :body or :query
137
+ # @option opts [Symbol, Hash, or callable] :mode (:header) the transmission mode of the Access Token parameter value:
138
+ # either one of :header, :body or :query; or a Hash with verb symbols as keys mapping to one of these symbols
139
+ # (e.g., `{get: :query, post: :header, delete: :header}`); or a callable that accepts a request-verb parameter
140
+ # and returns one of these three symbols.
59
141
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
142
+ #
143
+ # @example Verb-dependent Hash mode
144
+ # # Send token in query for GET, in header for POST/DELETE, in body for PUT/PATCH
145
+ # OAuth2::AccessToken.new(client, token, mode: {get: :query, post: :header, delete: :header, put: :body, patch: :body})
60
146
  # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
61
147
  # Access Token value in :body or :query transmission mode
148
+ # @option opts [String] :token_name (nil) the name of the response parameter that identifies the access token
149
+ # When nil one of TOKEN_KEY_LOOKUP will be used
62
150
  def initialize(client, token, opts = {})
63
151
  @client = client
64
152
  @token = token.to_s
65
-
66
153
  opts = opts.dup
67
154
  %i[refresh_token expires_in expires_at expires_latency].each do |arg|
68
155
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
@@ -70,22 +157,28 @@ module OAuth2
70
157
  no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
71
158
  if no_tokens
72
159
  if @client.options[:raise_errors]
73
- error = Error.new(opts)
74
- raise(error)
75
- else
76
- warn('OAuth2::AccessToken has no token')
160
+ raise Error.new({
161
+ error: "OAuth2::AccessToken has no token",
162
+ error_description: "Options are: #{opts.inspect}",
163
+ })
164
+ elsif !OAuth2.config.silence_no_tokens_warning
165
+ warn("OAuth2::AccessToken has no token")
77
166
  end
78
167
  end
79
168
  # @option opts [Fixnum, String] :expires is deprecated
80
- @expires_in ||= opts.delete('expires')
169
+ @expires_in ||= opts.delete("expires")
81
170
  @expires_in &&= @expires_in.to_i
82
171
  @expires_at &&= convert_expires_at(@expires_at)
83
172
  @expires_latency &&= @expires_latency.to_i
84
- @expires_at ||= Time.now.to_i + @expires_in if @expires_in
173
+ @expires_at ||= Time.now.to_i + @expires_in if @expires_in && !@expires_in.zero?
85
174
  @expires_at -= @expires_latency if @expires_latency
86
- @options = {mode: opts.delete(:mode) || :header,
87
- header_format: opts.delete(:header_format) || 'Bearer %s',
88
- param_name: opts.delete(:param_name) || 'access_token'}
175
+ @options = {
176
+ mode: opts.delete(:mode) || :header,
177
+ header_format: opts.delete(:header_format) || "Bearer %s",
178
+ param_name: opts.delete(:param_name) || "access_token",
179
+ }
180
+ @options[:token_name] = opts.delete(:token_name) if opts.key?(:token_name)
181
+
89
182
  @params = opts
90
183
  end
91
184
 
@@ -96,33 +189,40 @@ module OAuth2
96
189
  @params[key]
97
190
  end
98
191
 
99
- # Whether or not the token expires
192
+ # Whether the token expires
100
193
  #
101
194
  # @return [Boolean]
102
195
  def expires?
103
196
  !!@expires_at
104
197
  end
105
198
 
106
- # Whether or not the token is expired
199
+ # Check if token is expired
107
200
  #
108
- # @return [Boolean]
201
+ # @return [Boolean] true if the token is expired, false otherwise
109
202
  def expired?
110
203
  expires? && (expires_at <= Time.now.to_i)
111
204
  end
112
205
 
113
206
  # Refreshes the current Access Token
114
207
  #
115
- # @return [AccessToken] a new AccessToken
116
- # @note options should be carried over to the new AccessToken
117
- def refresh(params = {}, access_token_opts = {})
118
- raise('A refresh_token is not available') unless refresh_token
208
+ # @param [Hash] params additional params to pass to the refresh token request
209
+ # @param [Hash] access_token_opts options that will be passed to the AccessToken initialization
210
+ #
211
+ # @yield [opts] The block to modify the refresh token request options
212
+ # @yieldparam [Hash] opts The options hash that can be modified
213
+ #
214
+ # @return [OAuth2::AccessToken] a new AccessToken instance
215
+ #
216
+ # @note current token's options are carried over to the new AccessToken
217
+ def refresh(params = {}, access_token_opts = {}, &block)
218
+ raise OAuth2::Error.new({error: "A refresh_token is not available"}) unless refresh_token
119
219
 
120
- params[:grant_type] = 'refresh_token'
220
+ params[:grant_type] = "refresh_token"
121
221
  params[:refresh_token] = refresh_token
122
- new_token = @client.get_token(params, access_token_opts)
222
+ new_token = @client.get_token(params, access_token_opts, &block)
123
223
  new_token.options = options
124
224
  if new_token.refresh_token
125
- # Keep it, if there is one
225
+ # Keep it if there is one
126
226
  else
127
227
  new_token.refresh_token = refresh_token
128
228
  end
@@ -130,13 +230,90 @@ module OAuth2
130
230
  end
131
231
  # A compatibility alias
132
232
  # @note does not modify the receiver, so bang is not the default method
133
- alias refresh! refresh
233
+ alias_method :refresh!, :refresh
234
+
235
+ # Revokes the token at the authorization server
236
+ #
237
+ # @param [Hash] params additional parameters to be sent during revocation
238
+ # @option params [String, Symbol, nil] :token_type_hint ('access_token' or 'refresh_token') hint about which token to revoke
239
+ # @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method]
240
+ #
241
+ # @yield [req] The block is passed the request being made, allowing customization
242
+ # @yieldparam [Faraday::Request] req The request object that can be modified
243
+ #
244
+ # @return [OAuth2::Response] OAuth2::Response instance
245
+ #
246
+ # @api public
247
+ #
248
+ # @raise [OAuth2::Error] if token_type_hint is invalid or the specified token is not available
249
+ #
250
+ # @note If the token passed to the request
251
+ # is an access token, the server MAY revoke the respective refresh
252
+ # token as well.
253
+ # @note If the token passed to the request
254
+ # is a refresh token and the authorization server supports the
255
+ # revocation of access tokens, then the authorization server SHOULD
256
+ # also invalidate all access tokens based on the same authorization
257
+ # grant
258
+ # @note If the server responds with HTTP status code 503, your code must
259
+ # assume the token still exists and may retry after a reasonable delay.
260
+ # The server may include a "Retry-After" header in the response to
261
+ # indicate how long the service is expected to be unavailable to the
262
+ # requesting client.
263
+ #
264
+ # @see https://datatracker.ietf.org/doc/html/rfc7009
265
+ # @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
266
+ def revoke(params = {}, &block)
267
+ token_type_hint_orig = params.delete(:token_type_hint)
268
+ token_type_hint = nil
269
+ revoke_token = case token_type_hint_orig
270
+ when "access_token", :access_token
271
+ token_type_hint = "access_token"
272
+ token
273
+ when "refresh_token", :refresh_token
274
+ token_type_hint = "refresh_token"
275
+ refresh_token
276
+ when nil
277
+ if token
278
+ token_type_hint = "access_token"
279
+ token
280
+ elsif refresh_token
281
+ token_type_hint = "refresh_token"
282
+ refresh_token
283
+ end
284
+ else
285
+ 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."})
286
+ end
287
+ raise OAuth2::Error.new({error: "#{token_type_hint || "unknown token type"} is not available for revoking"}) unless revoke_token && !revoke_token.empty?
288
+
289
+ @client.revoke_token(revoke_token, token_type_hint, params, &block)
290
+ end
291
+ # A compatibility alias
292
+ # @note does not modify the receiver, so bang is not the default method
293
+ alias_method :revoke!, :revoke
134
294
 
135
295
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
136
296
  #
297
+ # @note Don't return expires_latency because it has already been deducted from expires_at
298
+ #
137
299
  # @return [Hash] a hash of AccessToken property values
138
300
  def to_hash
139
- params.merge(access_token: token, refresh_token: refresh_token, expires_at: expires_at)
301
+ hsh = {
302
+ access_token: token,
303
+ refresh_token: refresh_token,
304
+ expires_at: expires_at,
305
+ mode: options[:mode],
306
+ header_format: options[:header_format],
307
+ param_name: options[:param_name],
308
+ }
309
+ hsh[:token_name] = options[:token_name] if options.key?(:token_name)
310
+ # TODO: Switch when dropping Ruby < 2.5 support
311
+ # params.transform_keys(&:to_sym) # Ruby 2.5 only
312
+ # Old Ruby transform_keys alternative:
313
+ sheesh = @params.each_with_object({}) { |(key, value), memo|
314
+ memo[key.to_sym] = value
315
+ }
316
+ sheesh.merge(hsh)
140
317
  end
141
318
 
142
319
  # Make a request with the Access Token
@@ -144,9 +321,18 @@ module OAuth2
144
321
  # @param [Symbol] verb the HTTP request method
145
322
  # @param [String] path the HTTP URL path of the request
146
323
  # @param [Hash] opts the options to make the request with
147
- # @see Client#request
324
+ # @option opts [Hash] :params additional URL parameters
325
+ # @option opts [Hash, String] :body the request body
326
+ # @option opts [Hash] :headers request headers
327
+ #
328
+ # @yield [req] The block to modify the request
329
+ # @yieldparam [Faraday::Request] req The request object that can be modified
330
+ #
331
+ # @return [OAuth2::Response] the response from the request
332
+ #
333
+ # @see OAuth2::Client#request
148
334
  def request(verb, path, opts = {}, &block)
149
- configure_authentication!(opts)
335
+ configure_authentication!(opts, verb)
150
336
  @client.request(verb, path, opts, &block)
151
337
  end
152
338
 
@@ -187,29 +373,44 @@ module OAuth2
187
373
 
188
374
  # Get the headers hash (includes Authorization token)
189
375
  def headers
190
- {'Authorization' => options[:header_format] % token}
376
+ {"Authorization" => options[:header_format] % token}
191
377
  end
192
378
 
193
379
  private
194
380
 
195
- def configure_authentication!(opts)
196
- case options[:mode]
381
+ def configure_authentication!(opts, verb)
382
+ mode_opt = options[:mode]
383
+ param_name = options[:param_name]
384
+ mode =
385
+ if mode_opt.respond_to?(:call)
386
+ mode_opt.call(verb)
387
+ elsif mode_opt.is_a?(Hash)
388
+ key = verb.to_sym
389
+ # Try symbol key first, then string key; default to :header when missing
390
+ mode_opt[key] || mode_opt[key.to_s] || :header
391
+ else
392
+ mode_opt
393
+ end
394
+
395
+ case mode
197
396
  when :header
198
- opts[:headers] ||= {}
199
- opts[:headers].merge!(headers)
397
+ request_headers = opts[:headers] ||= {}
398
+ request_headers.merge!(headers)
200
399
  when :query
201
- opts[:params] ||= {}
202
- opts[:params][options[:param_name]] = token
400
+ # OAuth 2.1 note: Bearer tokens in the query string are omitted from the spec due to security risks.
401
+ # Prefer the default :header mode whenever possible.
402
+ request_params = opts[:params] ||= {}
403
+ request_params[param_name] = token
203
404
  when :body
204
- opts[:body] ||= {}
205
- if opts[:body].is_a?(Hash)
206
- opts[:body][options[:param_name]] = token
405
+ request_body = opts[:body] ||= {}
406
+ if request_body.is_a?(Hash)
407
+ request_body[param_name] = token
207
408
  else
208
- opts[:body] += "&#{options[:param_name]}=#{token}"
409
+ opts[:body] = "#{request_body}&#{param_name}=#{token}"
209
410
  end
210
411
  # @todo support for multi-part (file uploads)
211
412
  else
212
- raise("invalid :mode option of #{options[:mode]}")
413
+ raise("invalid :mode option of #{mode}")
213
414
  end
214
415
  end
215
416
 
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anonymous_loader"
4
+
5
+ module OAuth2
6
+ AUTH_SANITIZER = begin
7
+ auth_sanitizer_requirement = Gem::Requirement.new("~> 0.2", ">= 0.2.2")
8
+ auth_sanitizer_loader_namespace = AnonymousLoader.load_path(
9
+ gem_name: "auth-sanitizer",
10
+ require_path: "auth_sanitizer/loader.rb",
11
+ version_requirement: auth_sanitizer_requirement,
12
+ version_file: "auth/sanitizer/version.rb"
13
+ )
14
+
15
+ auth_sanitizer_loader_namespace.
16
+ const_get(:AuthSanitizer).
17
+ const_get(:Loader).
18
+ load_isolated
19
+ end
20
+ end
@@ -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,40 +51,59 @@ module OAuth2
36
51
  end
37
52
  end
38
53
 
39
- def self.encode_basic_auth(user, password)
40
- "Basic #{Base64.strict_encode64("#{user}:#{password}")}"
54
+ class << self
55
+ # Encodes a Basic Authorization header value for the provided credentials.
56
+ #
57
+ # @param [String] user The client identifier
58
+ # @param [String] password The client secret
59
+ # @return [String] The value to use for the Authorization header
60
+ def encode_basic_auth(user, password)
61
+ "Basic #{Base64.strict_encode64("#{user}:#{password}")}"
62
+ end
41
63
  end
42
64
 
43
65
  private
44
66
 
45
67
  # Adds client_id and client_secret request parameters if they are not
46
68
  # already set.
69
+ #
70
+ # @param [Hash] params Request parameters
71
+ # @return [Hash] Updated parameters including client_id and client_secret
47
72
  def apply_params_auth(params)
48
73
  result = {}
49
- result['client_id'] = id unless id.nil?
50
- result['client_secret'] = secret unless secret.nil?
74
+ result["client_id"] = id unless id.nil?
75
+ result["client_secret"] = secret unless secret.nil?
51
76
  result.merge(params)
52
77
  end
53
78
 
54
- # When using schemes that don't require the client_secret to be passed i.e TLS Client Auth,
79
+ # When using schemes that don't require the client_secret to be passed (e.g., TLS Client Auth),
55
80
  # we don't want to send the secret
81
+ #
82
+ # @param [Hash] params Request parameters
83
+ # @return [Hash] Updated parameters including only client_id
56
84
  def apply_client_id(params)
57
85
  result = {}
58
- result['client_id'] = id unless id.nil?
86
+ result["client_id"] = id unless id.nil?
59
87
  result.merge(params)
60
88
  end
61
89
 
62
90
  # Adds an `Authorization` header with Basic Auth credentials if and only if
63
91
  # it is not already set in the params.
92
+ #
93
+ # @param [Hash] params Request parameters (may include :headers)
94
+ # @return [Hash] Updated parameters with Authorization header
64
95
  def apply_basic_auth(params)
65
96
  headers = params.fetch(:headers, {})
66
97
  headers = basic_auth_header.merge(headers)
67
98
  params.merge(headers: headers)
68
99
  end
69
100
 
101
+ # Build the Basic Authorization header.
102
+ #
70
103
  # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
104
+ # @return [Hash] Header hash containing the Authorization entry
71
105
  def basic_auth_header
72
- {'Authorization' => self.class.encode_basic_auth(id, secret)}
106
+ {"Authorization" => self.class.encode_basic_auth(id, secret)}
73
107
  end
74
108
  end
75
109
  end