oauth2 2.0.9 → 2.0.11

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/SECURITY.md CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  | Version | Supported | EOL | Post-EOL / Enterprise |
6
6
  |----------|-----------|---------|---------------------------------------|
7
- | 2.latest | ✅ | 04/2024 | [Tidelift Subscription][tidelift-ref] |
8
- | 1.latest | ✅ | 04/2023 | [Tidelift Subscription][tidelift-ref] |
7
+ | 2.latest | ✅ | 04/2026 | [Tidelift Subscription][tidelift-ref] |
8
+ | 1.latest | ✅ | 10/2025 | [Tidelift Subscription][tidelift-ref] |
9
9
  | <= 1 | ⛔ | ⛔ | ⛔ |
10
10
 
11
11
  ### EOL Policy
@@ -1,27 +1,84 @@
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
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
+ 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:
25
82
  new(client, token, fresh)
26
83
  end
27
84
 
@@ -43,10 +100,31 @@ module OAuth2
43
100
 
44
101
  warn("OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (#{supported_keys}); using #{key.inspect}.")
45
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
46
114
  end
47
115
 
48
116
  # Initialize an AccessToken
49
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`.
127
+ #
50
128
  # @param [Client] client the OAuth2::Client instance
51
129
  # @param [String] token the Access Token value (optional, may not be used in refresh flows)
52
130
  # @param [Hash] opts the options to create the Access Token with
@@ -59,10 +137,11 @@ module OAuth2
59
137
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
60
138
  # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
61
139
  # Access Token value in :body or :query transmission mode
140
+ # @option opts [String] :token_name (nil) the name of the response parameter that identifies the access token
141
+ # When nil one of TOKEN_KEY_LOOKUP will be used
62
142
  def initialize(client, token, opts = {})
63
143
  @client = client
64
144
  @token = token.to_s
65
-
66
145
  opts = opts.dup
67
146
  %i[refresh_token expires_in expires_at expires_latency].each do |arg|
68
147
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
@@ -70,22 +149,28 @@ module OAuth2
70
149
  no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
71
150
  if no_tokens
72
151
  if @client.options[:raise_errors]
73
- error = Error.new(opts)
74
- raise(error)
75
- else
76
- warn('OAuth2::AccessToken has no token')
152
+ raise Error.new({
153
+ error: "OAuth2::AccessToken has no token",
154
+ error_description: "Options are: #{opts.inspect}",
155
+ })
156
+ elsif !OAuth2.config.silence_no_tokens_warning
157
+ warn("OAuth2::AccessToken has no token")
77
158
  end
78
159
  end
79
160
  # @option opts [Fixnum, String] :expires is deprecated
80
- @expires_in ||= opts.delete('expires')
161
+ @expires_in ||= opts.delete("expires")
81
162
  @expires_in &&= @expires_in.to_i
82
163
  @expires_at &&= convert_expires_at(@expires_at)
83
164
  @expires_latency &&= @expires_latency.to_i
84
- @expires_at ||= Time.now.to_i + @expires_in if @expires_in
165
+ @expires_at ||= Time.now.to_i + @expires_in if @expires_in && !@expires_in.zero?
85
166
  @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'}
167
+ @options = {
168
+ mode: opts.delete(:mode) || :header,
169
+ header_format: opts.delete(:header_format) || "Bearer %s",
170
+ param_name: opts.delete(:param_name) || "access_token",
171
+ }
172
+ @options[:token_name] = opts.delete(:token_name) if opts.key?(:token_name)
173
+
89
174
  @params = opts
90
175
  end
91
176
 
@@ -96,33 +181,40 @@ module OAuth2
96
181
  @params[key]
97
182
  end
98
183
 
99
- # Whether or not the token expires
184
+ # Whether the token expires
100
185
  #
101
186
  # @return [Boolean]
102
187
  def expires?
103
188
  !!@expires_at
104
189
  end
105
190
 
106
- # Whether or not the token is expired
191
+ # Check if token is expired
107
192
  #
108
- # @return [Boolean]
193
+ # @return [Boolean] true if the token is expired, false otherwise
109
194
  def expired?
110
195
  expires? && (expires_at <= Time.now.to_i)
111
196
  end
112
197
 
113
198
  # Refreshes the current Access Token
114
199
  #
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
200
+ # @param [Hash] params additional params to pass to the refresh token request
201
+ # @param [Hash] access_token_opts options that will be passed to the AccessToken initialization
202
+ #
203
+ # @yield [opts] The block to modify the refresh token request options
204
+ # @yieldparam [Hash] opts The options hash that can be modified
205
+ #
206
+ # @return [OAuth2::AccessToken] a new AccessToken instance
207
+ #
208
+ # @note current token's options are carried over to the new AccessToken
209
+ def refresh(params = {}, access_token_opts = {}, &block)
210
+ raise OAuth2::Error.new({error: "A refresh_token is not available"}) unless refresh_token
119
211
 
120
- params[:grant_type] = 'refresh_token'
212
+ params[:grant_type] = "refresh_token"
121
213
  params[:refresh_token] = refresh_token
122
- new_token = @client.get_token(params, access_token_opts)
214
+ new_token = @client.get_token(params, access_token_opts, &block)
123
215
  new_token.options = options
124
216
  if new_token.refresh_token
125
- # Keep it, if there is one
217
+ # Keep it if there is one
126
218
  else
127
219
  new_token.refresh_token = refresh_token
128
220
  end
@@ -130,13 +222,90 @@ module OAuth2
130
222
  end
131
223
  # A compatibility alias
132
224
  # @note does not modify the receiver, so bang is not the default method
133
- alias refresh! refresh
225
+ alias_method :refresh!, :refresh
226
+
227
+ # Revokes the token at the authorization server
228
+ #
229
+ # @param [Hash] params additional parameters to be sent during revocation
230
+ # @option params [String, Symbol, nil] :token_type_hint ('access_token' or 'refresh_token') hint about which token to revoke
231
+ # @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method]
232
+ #
233
+ # @yield [req] The block is passed the request being made, allowing customization
234
+ # @yieldparam [Faraday::Request] req The request object that can be modified
235
+ #
236
+ # @return [OAuth2::Response] OAuth2::Response instance
237
+ #
238
+ # @api public
239
+ #
240
+ # @raise [OAuth2::Error] if token_type_hint is invalid or the specified token is not available
241
+ #
242
+ # @note If the token passed to the request
243
+ # is an access token, the server MAY revoke the respective refresh
244
+ # token as well.
245
+ # @note If the token passed to the request
246
+ # is a refresh token and the authorization server supports the
247
+ # revocation of access tokens, then the authorization server SHOULD
248
+ # also invalidate all access tokens based on the same authorization
249
+ # grant
250
+ # @note If the server responds with HTTP status code 503, your code must
251
+ # assume the token still exists and may retry after a reasonable delay.
252
+ # The server may include a "Retry-After" header in the response to
253
+ # indicate how long the service is expected to be unavailable to the
254
+ # requesting client.
255
+ #
256
+ # @see https://datatracker.ietf.org/doc/html/rfc7009
257
+ # @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
258
+ def revoke(params = {}, &block)
259
+ token_type_hint_orig = params.delete(:token_type_hint)
260
+ token_type_hint = nil
261
+ revoke_token = case token_type_hint_orig
262
+ when "access_token", :access_token
263
+ token_type_hint = "access_token"
264
+ token
265
+ when "refresh_token", :refresh_token
266
+ token_type_hint = "refresh_token"
267
+ refresh_token
268
+ when nil
269
+ if token
270
+ token_type_hint = "access_token"
271
+ token
272
+ elsif refresh_token
273
+ token_type_hint = "refresh_token"
274
+ refresh_token
275
+ end
276
+ else
277
+ 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."})
278
+ end
279
+ raise OAuth2::Error.new({error: "#{token_type_hint || "unknown token type"} is not available for revoking"}) unless revoke_token && !revoke_token.empty?
280
+
281
+ @client.revoke_token(revoke_token, token_type_hint, params, &block)
282
+ end
283
+ # A compatibility alias
284
+ # @note does not modify the receiver, so bang is not the default method
285
+ alias_method :revoke!, :revoke
134
286
 
135
287
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
136
288
  #
289
+ # @note Don't return expires_latency because it has already been deducted from expires_at
290
+ #
137
291
  # @return [Hash] a hash of AccessToken property values
138
292
  def to_hash
139
- params.merge(access_token: token, refresh_token: refresh_token, expires_at: expires_at)
293
+ hsh = {
294
+ access_token: token,
295
+ refresh_token: refresh_token,
296
+ expires_at: expires_at,
297
+ mode: options[:mode],
298
+ header_format: options[:header_format],
299
+ param_name: options[:param_name],
300
+ }
301
+ hsh[:token_name] = options[:token_name] if options.key?(:token_name)
302
+ # TODO: Switch when dropping Ruby < 2.5 support
303
+ # params.transform_keys(&:to_sym) # Ruby 2.5 only
304
+ # Old Ruby transform_keys alternative:
305
+ sheesh = @params.each_with_object({}) { |(k, v), memo|
306
+ memo[k.to_sym] = v
307
+ }
308
+ sheesh.merge(hsh)
140
309
  end
141
310
 
142
311
  # Make a request with the Access Token
@@ -144,7 +313,16 @@ module OAuth2
144
313
  # @param [Symbol] verb the HTTP request method
145
314
  # @param [String] path the HTTP URL path of the request
146
315
  # @param [Hash] opts the options to make the request with
147
- # @see Client#request
316
+ # @option opts [Hash] :params additional URL parameters
317
+ # @option opts [Hash, String] :body the request body
318
+ # @option opts [Hash] :headers request headers
319
+ #
320
+ # @yield [req] The block to modify the request
321
+ # @yieldparam [Faraday::Request] req The request object that can be modified
322
+ #
323
+ # @return [OAuth2::Response] the response from the request
324
+ #
325
+ # @see OAuth2::Client#request
148
326
  def request(verb, path, opts = {}, &block)
149
327
  configure_authentication!(opts)
150
328
  @client.request(verb, path, opts, &block)
@@ -187,7 +365,7 @@ module OAuth2
187
365
 
188
366
  # Get the headers hash (includes Authorization token)
189
367
  def headers
190
- {'Authorization' => options[:header_format] % token}
368
+ {"Authorization" => options[:header_format] % token}
191
369
  end
192
370
 
193
371
  private
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'base64'
3
+ require "base64"
4
4
 
5
5
  module OAuth2
6
6
  class Authenticator
7
+ include FilteredAttributes
8
+
7
9
  attr_reader :mode, :id, :secret
10
+ filtered_attributes :secret
8
11
 
9
12
  def initialize(id, secret, mode)
10
13
  @id = id
@@ -14,7 +17,7 @@ module OAuth2
14
17
 
15
18
  # Apply the request credentials used to authenticate to the Authorization Server
16
19
  #
17
- # Depending on configuration, this might be as request params or as an
20
+ # Depending on the configuration, this might be as request params or as an
18
21
  # Authorization header.
19
22
  #
20
23
  # User-provided params and header take precedence.
@@ -46,8 +49,8 @@ module OAuth2
46
49
  # already set.
47
50
  def apply_params_auth(params)
48
51
  result = {}
49
- result['client_id'] = id unless id.nil?
50
- result['client_secret'] = secret unless secret.nil?
52
+ result["client_id"] = id unless id.nil?
53
+ result["client_secret"] = secret unless secret.nil?
51
54
  result.merge(params)
52
55
  end
53
56
 
@@ -55,7 +58,7 @@ module OAuth2
55
58
  # we don't want to send the secret
56
59
  def apply_client_id(params)
57
60
  result = {}
58
- result['client_id'] = id unless id.nil?
61
+ result["client_id"] = id unless id.nil?
59
62
  result.merge(params)
60
63
  end
61
64
 
@@ -69,7 +72,7 @@ module OAuth2
69
72
 
70
73
  # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
71
74
  def basic_auth_header
72
- {'Authorization' => self.class.encode_basic_auth(id, secret)}
75
+ {"Authorization" => self.class.encode_basic_auth(id, secret)}
73
76
  end
74
77
  end
75
78
  end