oauth2 2.0.9 → 2.0.10

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
@@ -6,22 +6,55 @@ module OAuth2
6
6
  TOKEN_KEYS_SYM = %i[access_token id_token token accessToken idToken].freeze
7
7
  TOKEN_KEY_LOOKUP = TOKEN_KEYS_STR + TOKEN_KEYS_SYM
8
8
 
9
+ include FilteredAttributes
10
+
9
11
  attr_reader :client, :token, :expires_in, :expires_at, :expires_latency, :params
10
12
  attr_accessor :options, :refresh_token, :response
13
+ filtered_attributes :token, :refresh_token
11
14
 
12
15
  class << self
13
16
  # Initializes an AccessToken from a Hash
14
17
  #
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
18
+ # @param [OAuth2::Client] client the OAuth2::Client instance
19
+ # @param [Hash] hash a hash containing the token and other properties
20
+ # @option hash [String] 'access_token' the access token value
21
+ # @option hash [String] 'id_token' alternative key for the access token value
22
+ # @option hash [String] 'token' alternative key for the access token value
23
+ # @option hash [String] 'refresh_token' (optional) the refresh token value
24
+ # @option hash [Integer, String] 'expires_in' (optional) number of seconds until token expires
25
+ # @option hash [Integer, String] 'expires_at' (optional) epoch time in seconds when token expires
26
+ # @option hash [Integer, String] 'expires_latency' (optional) seconds to reduce token validity by
27
+ #
28
+ # @return [OAuth2::AccessToken] the initialized AccessToken
29
+ #
30
+ # @note The method will use the first found token key in the following order:
31
+ # 'access_token', 'id_token', 'token' (or their symbolic versions)
32
+ # @note If multiple token keys are present, a warning will be issued unless
33
+ # OAuth2.config.silence_extra_tokens_warning is true
34
+ # @note If no token keys are present, a warning will be issued unless
35
+ # OAuth2.config.silence_no_tokens_warning is true
36
+ # @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option.
37
+ # @note If snaky key conversion is being used, token_name needs to match the converted key.
38
+ #
39
+ # @example
40
+ # hash = { 'access_token' => 'token_value', 'refresh_token' => 'refresh_value' }
41
+ # access_token = OAuth2::AccessToken.from_hash(client, hash)
19
42
  def from_hash(client, hash)
20
43
  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)
44
+ # If token_name is present, then use that key name
45
+ key =
46
+ if fresh.key?(:token_name)
47
+ t_key = fresh[:token_name]
48
+ no_tokens_warning(fresh, t_key)
49
+ t_key
50
+ else
51
+ # Otherwise, if one of the supported default keys is present, use whichever has precedence
52
+ supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
53
+ t_key = supported_keys[0]
54
+ extra_tokens_warning(supported_keys, t_key)
55
+ t_key
56
+ end
57
+ token = fresh.delete(key) || ""
25
58
  new(client, token, fresh)
26
59
  end
27
60
 
@@ -43,10 +76,31 @@ module OAuth2
43
76
 
44
77
  warn("OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (#{supported_keys}); using #{key.inspect}.")
45
78
  end
79
+
80
+ def no_tokens_warning(hash, key)
81
+ return if OAuth2.config.silence_no_tokens_warning
82
+ return if key && hash.key?(key)
83
+
84
+ warn(%[
85
+ OAuth2::AccessToken#from_hash key mismatch.
86
+ Custom token_name (#{key}) is not found in (#{hash.keys})
87
+ You may need to set `snaky: false`. See inline documentation for more info.
88
+ ])
89
+ end
46
90
  end
47
91
 
48
92
  # Initialize an AccessToken
49
93
  #
94
+ # @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option.
95
+ # @note If no token is provided, the AccessToken will be considered invalid.
96
+ # This is to prevent the possibility of a token being accidentally
97
+ # created with no token value.
98
+ # If you want to create an AccessToken with no token value,
99
+ # you can pass in an empty string or nil for the token value.
100
+ # If you want to create an AccessToken with no token value and
101
+ # no refresh token, you can pass in an empty string or nil for the
102
+ # token value and nil for the refresh token, and `raise_errors: false`.
103
+ #
50
104
  # @param [Client] client the OAuth2::Client instance
51
105
  # @param [String] token the Access Token value (optional, may not be used in refresh flows)
52
106
  # @param [Hash] opts the options to create the Access Token with
@@ -59,10 +113,11 @@ module OAuth2
59
113
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
60
114
  # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
61
115
  # Access Token value in :body or :query transmission mode
116
+ # @option opts [String] :token_name (nil) the name of the response parameter that identifies the access token
117
+ # When nil one of TOKEN_KEY_LOOKUP will be used
62
118
  def initialize(client, token, opts = {})
63
119
  @client = client
64
120
  @token = token.to_s
65
-
66
121
  opts = opts.dup
67
122
  %i[refresh_token expires_in expires_at expires_latency].each do |arg|
68
123
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
@@ -70,22 +125,28 @@ module OAuth2
70
125
  no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
71
126
  if no_tokens
72
127
  if @client.options[:raise_errors]
73
- error = Error.new(opts)
74
- raise(error)
75
- else
76
- warn('OAuth2::AccessToken has no token')
128
+ raise Error.new({
129
+ error: "OAuth2::AccessToken has no token",
130
+ error_description: "Options are: #{opts.inspect}",
131
+ })
132
+ elsif !OAuth2.config.silence_no_tokens_warning
133
+ warn("OAuth2::AccessToken has no token")
77
134
  end
78
135
  end
79
136
  # @option opts [Fixnum, String] :expires is deprecated
80
- @expires_in ||= opts.delete('expires')
137
+ @expires_in ||= opts.delete("expires")
81
138
  @expires_in &&= @expires_in.to_i
82
139
  @expires_at &&= convert_expires_at(@expires_at)
83
140
  @expires_latency &&= @expires_latency.to_i
84
- @expires_at ||= Time.now.to_i + @expires_in if @expires_in
141
+ @expires_at ||= Time.now.to_i + @expires_in if @expires_in && !@expires_in.zero?
85
142
  @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'}
143
+ @options = {
144
+ mode: opts.delete(:mode) || :header,
145
+ header_format: opts.delete(:header_format) || "Bearer %s",
146
+ param_name: opts.delete(:param_name) || "access_token",
147
+ }
148
+ @options[:token_name] = opts.delete(:token_name) if opts.key?(:token_name)
149
+
89
150
  @params = opts
90
151
  end
91
152
 
@@ -96,33 +157,40 @@ module OAuth2
96
157
  @params[key]
97
158
  end
98
159
 
99
- # Whether or not the token expires
160
+ # Whether the token expires
100
161
  #
101
162
  # @return [Boolean]
102
163
  def expires?
103
164
  !!@expires_at
104
165
  end
105
166
 
106
- # Whether or not the token is expired
167
+ # Check if token is expired
107
168
  #
108
- # @return [Boolean]
169
+ # @return [Boolean] true if the token is expired, false otherwise
109
170
  def expired?
110
171
  expires? && (expires_at <= Time.now.to_i)
111
172
  end
112
173
 
113
174
  # Refreshes the current Access Token
114
175
  #
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
176
+ # @param [Hash] params additional params to pass to the refresh token request
177
+ # @param [Hash] access_token_opts options that will be passed to the AccessToken initialization
178
+ #
179
+ # @yield [opts] The block to modify the refresh token request options
180
+ # @yieldparam [Hash] opts The options hash that can be modified
181
+ #
182
+ # @return [OAuth2::AccessToken] a new AccessToken instance
183
+ #
184
+ # @note current token's options are carried over to the new AccessToken
185
+ def refresh(params = {}, access_token_opts = {}, &block)
186
+ raise OAuth2::Error.new({error: "A refresh_token is not available"}) unless refresh_token
119
187
 
120
- params[:grant_type] = 'refresh_token'
188
+ params[:grant_type] = "refresh_token"
121
189
  params[:refresh_token] = refresh_token
122
- new_token = @client.get_token(params, access_token_opts)
190
+ new_token = @client.get_token(params, access_token_opts, &block)
123
191
  new_token.options = options
124
192
  if new_token.refresh_token
125
- # Keep it, if there is one
193
+ # Keep it if there is one
126
194
  else
127
195
  new_token.refresh_token = refresh_token
128
196
  end
@@ -130,13 +198,90 @@ module OAuth2
130
198
  end
131
199
  # A compatibility alias
132
200
  # @note does not modify the receiver, so bang is not the default method
133
- alias refresh! refresh
201
+ alias_method :refresh!, :refresh
202
+
203
+ # Revokes the token at the authorization server
204
+ #
205
+ # @param [Hash] params additional parameters to be sent during revocation
206
+ # @option params [String, Symbol, nil] :token_type_hint ('access_token' or 'refresh_token') hint about which token to revoke
207
+ # @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method]
208
+ #
209
+ # @yield [req] The block is passed the request being made, allowing customization
210
+ # @yieldparam [Faraday::Request] req The request object that can be modified
211
+ #
212
+ # @return [OAuth2::Response] OAuth2::Response instance
213
+ #
214
+ # @api public
215
+ #
216
+ # @raise [OAuth2::Error] if token_type_hint is invalid or the specified token is not available
217
+ #
218
+ # @note If the token passed to the request
219
+ # is an access token, the server MAY revoke the respective refresh
220
+ # token as well.
221
+ # @note If the token passed to the request
222
+ # is a refresh token and the authorization server supports the
223
+ # revocation of access tokens, then the authorization server SHOULD
224
+ # also invalidate all access tokens based on the same authorization
225
+ # grant
226
+ # @note If the server responds with HTTP status code 503, your code must
227
+ # assume the token still exists and may retry after a reasonable delay.
228
+ # The server may include a "Retry-After" header in the response to
229
+ # indicate how long the service is expected to be unavailable to the
230
+ # requesting client.
231
+ #
232
+ # @see https://datatracker.ietf.org/doc/html/rfc7009
233
+ # @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
234
+ def revoke(params = {}, &block)
235
+ token_type_hint_orig = params.delete(:token_type_hint)
236
+ token_type_hint = nil
237
+ revoke_token = case token_type_hint_orig
238
+ when "access_token", :access_token
239
+ token_type_hint = "access_token"
240
+ token
241
+ when "refresh_token", :refresh_token
242
+ token_type_hint = "refresh_token"
243
+ refresh_token
244
+ when nil
245
+ if token
246
+ token_type_hint = "access_token"
247
+ token
248
+ elsif refresh_token
249
+ token_type_hint = "refresh_token"
250
+ refresh_token
251
+ end
252
+ else
253
+ 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."})
254
+ end
255
+ raise OAuth2::Error.new({error: "#{token_type_hint || "unknown token type"} is not available for revoking"}) unless revoke_token && !revoke_token.empty?
256
+
257
+ @client.revoke_token(revoke_token, token_type_hint, params, &block)
258
+ end
259
+ # A compatibility alias
260
+ # @note does not modify the receiver, so bang is not the default method
261
+ alias_method :revoke!, :revoke
134
262
 
135
263
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
136
264
  #
265
+ # @note Don't return expires_latency because it has already been deducted from expires_at
266
+ #
137
267
  # @return [Hash] a hash of AccessToken property values
138
268
  def to_hash
139
- params.merge(access_token: token, refresh_token: refresh_token, expires_at: expires_at)
269
+ hsh = {
270
+ access_token: token,
271
+ refresh_token: refresh_token,
272
+ expires_at: expires_at,
273
+ mode: options[:mode],
274
+ header_format: options[:header_format],
275
+ param_name: options[:param_name],
276
+ }
277
+ hsh[:token_name] = options[:token_name] if options.key?(:token_name)
278
+ # TODO: Switch when dropping Ruby < 2.5 support
279
+ # params.transform_keys(&:to_sym) # Ruby 2.5 only
280
+ # Old Ruby transform_keys alternative:
281
+ sheesh = @params.each_with_object({}) { |(k, v), memo|
282
+ memo[k.to_sym] = v
283
+ }
284
+ sheesh.merge(hsh)
140
285
  end
141
286
 
142
287
  # Make a request with the Access Token
@@ -144,7 +289,16 @@ module OAuth2
144
289
  # @param [Symbol] verb the HTTP request method
145
290
  # @param [String] path the HTTP URL path of the request
146
291
  # @param [Hash] opts the options to make the request with
147
- # @see Client#request
292
+ # @option opts [Hash] :params additional URL parameters
293
+ # @option opts [Hash, String] :body the request body
294
+ # @option opts [Hash] :headers request headers
295
+ #
296
+ # @yield [req] The block to modify the request
297
+ # @yieldparam [Faraday::Request] req The request object that can be modified
298
+ #
299
+ # @return [OAuth2::Response] the response from the request
300
+ #
301
+ # @see OAuth2::Client#request
148
302
  def request(verb, path, opts = {}, &block)
149
303
  configure_authentication!(opts)
150
304
  @client.request(verb, path, opts, &block)
@@ -187,7 +341,7 @@ module OAuth2
187
341
 
188
342
  # Get the headers hash (includes Authorization token)
189
343
  def headers
190
- {'Authorization' => options[:header_format] % token}
344
+ {"Authorization" => options[:header_format] % token}
191
345
  end
192
346
 
193
347
  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