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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +301 -168
- data/CODE_OF_CONDUCT.md +0 -0
- data/CONTRIBUTING.md +126 -31
- data/{LICENSE → LICENSE.txt} +1 -1
- data/README.md +619 -245
- data/SECURITY.md +2 -2
- data/lib/oauth2/access_token.rb +186 -32
- data/lib/oauth2/authenticator.rb +9 -6
- data/lib/oauth2/client.rb +291 -96
- data/lib/oauth2/error.rb +14 -14
- data/lib/oauth2/filtered_attributes.rb +31 -0
- data/lib/oauth2/response.rb +17 -13
- data/lib/oauth2/strategy/assertion.rb +4 -4
- data/lib/oauth2/strategy/auth_code.rb +3 -3
- data/lib/oauth2/strategy/base.rb +0 -0
- data/lib/oauth2/strategy/client_credentials.rb +2 -2
- data/lib/oauth2/strategy/implicit.rb +3 -3
- data/lib/oauth2/strategy/password.rb +6 -4
- data/lib/oauth2/version.rb +1 -1
- data/lib/oauth2.rb +23 -18
- data.tar.gz.sig +0 -0
- metadata +188 -67
- metadata.gz.sig +0 -0
data/SECURITY.md
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
|
5
5
|
| Version | Supported | EOL | Post-EOL / Enterprise |
|
6
6
|
|----------|-----------|---------|---------------------------------------|
|
7
|
-
| 2.latest | ✅ | 04/
|
8
|
-
| 1.latest | ✅ |
|
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
|
data/lib/oauth2/access_token.rb
CHANGED
@@ -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
|
17
|
-
# @option hash [String
|
18
|
-
# @
|
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
|
-
|
22
|
-
key =
|
23
|
-
|
24
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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(
|
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 = {
|
87
|
-
|
88
|
-
|
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
|
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
|
-
#
|
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
|
-
# @
|
116
|
-
# @
|
117
|
-
|
118
|
-
|
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] =
|
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
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
{
|
344
|
+
{"Authorization" => options[:header_format] % token}
|
191
345
|
end
|
192
346
|
|
193
347
|
private
|
data/lib/oauth2/authenticator.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
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[
|
50
|
-
result[
|
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[
|
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
|
-
{
|
75
|
+
{"Authorization" => self.class.encode_basic_auth(id, secret)}
|
73
76
|
end
|
74
77
|
end
|
75
78
|
end
|