oauth2 1.4.4 → 2.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +242 -64
- data/CODE_OF_CONDUCT.md +105 -46
- data/CONTRIBUTING.md +27 -1
- data/LICENSE +1 -1
- data/README.md +445 -140
- data/SECURITY.md +26 -0
- data/lib/oauth2/access_token.rb +70 -28
- data/lib/oauth2/authenticator.rb +12 -5
- data/lib/oauth2/client.rb +208 -65
- data/lib/oauth2/error.rb +43 -24
- data/lib/oauth2/response.rb +81 -22
- data/lib/oauth2/strategy/assertion.rb +66 -39
- data/lib/oauth2/strategy/auth_code.rb +16 -3
- data/lib/oauth2/strategy/base.rb +2 -0
- data/lib/oauth2/strategy/client_credentials.rb +4 -2
- data/lib/oauth2/strategy/implicit.rb +10 -1
- data/lib/oauth2/strategy/password.rb +5 -3
- data/lib/oauth2/version.rb +3 -55
- data/lib/oauth2.rb +29 -1
- metadata +91 -105
- data/.document +0 -5
- data/.gitignore +0 -19
- data/.jrubyrc +0 -1
- data/.rspec +0 -2
- data/.rubocop.yml +0 -80
- data/.rubocop_rspec.yml +0 -26
- data/.rubocop_todo.yml +0 -15
- data/.ruby-version +0 -1
- data/.travis.yml +0 -87
- data/Gemfile +0 -40
- data/Rakefile +0 -45
- data/gemfiles/jruby_1.7.gemfile +0 -11
- data/gemfiles/jruby_9.0.gemfile +0 -7
- data/gemfiles/jruby_9.1.gemfile +0 -3
- data/gemfiles/jruby_9.2.gemfile +0 -3
- data/gemfiles/jruby_head.gemfile +0 -3
- data/gemfiles/ruby_1.9.gemfile +0 -11
- data/gemfiles/ruby_2.0.gemfile +0 -6
- data/gemfiles/ruby_2.1.gemfile +0 -6
- data/gemfiles/ruby_2.2.gemfile +0 -3
- data/gemfiles/ruby_2.3.gemfile +0 -3
- data/gemfiles/ruby_2.4.gemfile +0 -3
- data/gemfiles/ruby_2.5.gemfile +0 -3
- data/gemfiles/ruby_2.6.gemfile +0 -9
- data/gemfiles/ruby_2.7.gemfile +0 -9
- data/gemfiles/ruby_head.gemfile +0 -9
- data/gemfiles/truffleruby.gemfile +0 -3
- data/lib/oauth2/mac_token.rb +0 -122
- data/oauth2.gemspec +0 -52
data/SECURITY.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Security Policy
|
2
|
+
|
3
|
+
## Supported Versions
|
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 | ⛔ | ⛔ | ⛔ |
|
10
|
+
|
11
|
+
### EOL Policy
|
12
|
+
|
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).
|
18
|
+
Tidelift will coordinate the fix and disclosure.
|
19
|
+
|
20
|
+
## OAuth2 for Enterprise
|
21
|
+
|
22
|
+
Available as part of the Tidelift Subscription.
|
23
|
+
|
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]
|
25
|
+
|
26
|
+
[tidelift-ref]: https://tidelift.com/subscription/pkg/rubygems-oauth2?utm_source=rubygems-oauth2&utm_medium=referral&utm_campaign=enterprise&utm_term=repo
|
data/lib/oauth2/access_token.rb
CHANGED
@@ -1,56 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module OAuth2
|
2
|
-
class AccessToken
|
3
|
-
|
4
|
-
|
4
|
+
class AccessToken # rubocop:disable Metrics/ClassLength
|
5
|
+
TOKEN_KEYS_STR = %w[access_token id_token token accessToken idToken].freeze
|
6
|
+
TOKEN_KEYS_SYM = %i[access_token id_token token accessToken idToken].freeze
|
7
|
+
TOKEN_KEY_LOOKUP = TOKEN_KEYS_STR + TOKEN_KEYS_SYM
|
8
|
+
|
9
|
+
attr_reader :client, :token, :expires_in, :expires_at, :expires_latency, :params
|
10
|
+
attr_accessor :options, :refresh_token, :response
|
5
11
|
|
6
12
|
class << self
|
7
13
|
# Initializes an AccessToken from a Hash
|
8
14
|
#
|
9
|
-
# @param [Client] the OAuth2::Client instance
|
10
|
-
# @param [Hash] a hash of AccessToken property values
|
11
|
-
# @
|
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
|
12
19
|
def from_hash(client, hash)
|
13
|
-
|
14
|
-
|
20
|
+
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)
|
25
|
+
new(client, token, fresh)
|
15
26
|
end
|
16
27
|
|
17
28
|
# Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
|
18
29
|
#
|
19
30
|
# @param [Client] client the OAuth2::Client instance
|
20
31
|
# @param [String] kvform the application/x-www-form-urlencoded string
|
21
|
-
# @return [AccessToken] the
|
32
|
+
# @return [AccessToken] the initialized AccessToken
|
22
33
|
def from_kvform(client, kvform)
|
23
34
|
from_hash(client, Rack::Utils.parse_query(kvform))
|
24
35
|
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Having too many is sus, and may lead to bugs. Having none is fine (e.g. refresh flow doesn't need a token).
|
40
|
+
def extra_tokens_warning(supported_keys, key)
|
41
|
+
return if OAuth2.config.silence_extra_tokens_warning
|
42
|
+
return if supported_keys.length <= 1
|
43
|
+
|
44
|
+
warn("OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (#{supported_keys}); using #{key.inspect}.")
|
45
|
+
end
|
25
46
|
end
|
26
47
|
|
27
|
-
#
|
48
|
+
# Initialize an AccessToken
|
28
49
|
#
|
29
50
|
# @param [Client] client the OAuth2::Client instance
|
30
|
-
# @param [String] token the Access Token value
|
51
|
+
# @param [String] token the Access Token value (optional, may not be used in refresh flows)
|
31
52
|
# @param [Hash] opts the options to create the Access Token with
|
32
53
|
# @option opts [String] :refresh_token (nil) the refresh_token value
|
33
54
|
# @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
|
34
55
|
# @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
|
56
|
+
# @option opts [FixNum, String] :expires_latency (nil) the number of seconds by which AccessToken validity will be reduced to offset latency, @version 2.0+
|
35
57
|
# @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
|
36
58
|
# one of :header, :body or :query
|
37
59
|
# @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
|
38
60
|
# @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
|
39
61
|
# Access Token value in :body or :query transmission mode
|
40
|
-
def initialize(client, token, opts = {})
|
62
|
+
def initialize(client, token, opts = {})
|
41
63
|
@client = client
|
42
64
|
@token = token.to_s
|
65
|
+
|
43
66
|
opts = opts.dup
|
44
|
-
[
|
67
|
+
%i[refresh_token expires_in expires_at expires_latency].each do |arg|
|
45
68
|
instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
|
46
69
|
end
|
70
|
+
no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
|
71
|
+
if no_tokens
|
72
|
+
if @client.options[:raise_errors]
|
73
|
+
error = Error.new(opts)
|
74
|
+
raise(error)
|
75
|
+
else
|
76
|
+
warn('OAuth2::AccessToken has no token')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
# @option opts [Fixnum, String] :expires is deprecated
|
47
80
|
@expires_in ||= opts.delete('expires')
|
48
81
|
@expires_in &&= @expires_in.to_i
|
49
82
|
@expires_at &&= convert_expires_at(@expires_at)
|
83
|
+
@expires_latency &&= @expires_latency.to_i
|
50
84
|
@expires_at ||= Time.now.to_i + @expires_in if @expires_in
|
51
|
-
@
|
52
|
-
|
53
|
-
:
|
85
|
+
@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'}
|
54
89
|
@params = opts
|
55
90
|
end
|
56
91
|
|
@@ -72,28 +107,36 @@ module OAuth2
|
|
72
107
|
#
|
73
108
|
# @return [Boolean]
|
74
109
|
def expired?
|
75
|
-
expires? && (expires_at
|
110
|
+
expires? && (expires_at <= Time.now.to_i)
|
76
111
|
end
|
77
112
|
|
78
113
|
# Refreshes the current Access Token
|
79
114
|
#
|
80
115
|
# @return [AccessToken] a new AccessToken
|
81
116
|
# @note options should be carried over to the new AccessToken
|
82
|
-
def refresh
|
117
|
+
def refresh(params = {}, access_token_opts = {})
|
83
118
|
raise('A refresh_token is not available') unless refresh_token
|
119
|
+
|
84
120
|
params[:grant_type] = 'refresh_token'
|
85
121
|
params[:refresh_token] = refresh_token
|
86
|
-
new_token = @client.get_token(params)
|
122
|
+
new_token = @client.get_token(params, access_token_opts)
|
87
123
|
new_token.options = options
|
88
|
-
|
124
|
+
if new_token.refresh_token
|
125
|
+
# Keep it, if there is one
|
126
|
+
else
|
127
|
+
new_token.refresh_token = refresh_token
|
128
|
+
end
|
89
129
|
new_token
|
90
130
|
end
|
131
|
+
# A compatibility alias
|
132
|
+
# @note does not modify the receiver, so bang is not the default method
|
133
|
+
alias refresh! refresh
|
91
134
|
|
92
135
|
# Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
|
93
136
|
#
|
94
137
|
# @return [Hash] a hash of AccessToken property values
|
95
138
|
def to_hash
|
96
|
-
params.merge(:
|
139
|
+
params.merge(access_token: token, refresh_token: refresh_token, expires_at: expires_at)
|
97
140
|
end
|
98
141
|
|
99
142
|
# Make a request with the Access Token
|
@@ -101,7 +144,7 @@ module OAuth2
|
|
101
144
|
# @param [Symbol] verb the HTTP request method
|
102
145
|
# @param [String] path the HTTP URL path of the request
|
103
146
|
# @param [Hash] opts the options to make the request with
|
104
|
-
#
|
147
|
+
# @see Client#request
|
105
148
|
def request(verb, path, opts = {}, &block)
|
106
149
|
configure_authentication!(opts)
|
107
150
|
@client.request(verb, path, opts, &block)
|
@@ -149,7 +192,7 @@ module OAuth2
|
|
149
192
|
|
150
193
|
private
|
151
194
|
|
152
|
-
def configure_authentication!(opts)
|
195
|
+
def configure_authentication!(opts)
|
153
196
|
case options[:mode]
|
154
197
|
when :header
|
155
198
|
opts[:headers] ||= {}
|
@@ -162,7 +205,7 @@ module OAuth2
|
|
162
205
|
if opts[:body].is_a?(Hash)
|
163
206
|
opts[:body][options[:param_name]] = token
|
164
207
|
else
|
165
|
-
opts[:body]
|
208
|
+
opts[:body] += "&#{options[:param_name]}=#{token}"
|
166
209
|
end
|
167
210
|
# @todo support for multi-part (file uploads)
|
168
211
|
else
|
@@ -171,10 +214,9 @@ module OAuth2
|
|
171
214
|
end
|
172
215
|
|
173
216
|
def convert_expires_at(expires_at)
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
expires_at_i
|
217
|
+
Time.iso8601(expires_at.to_s).to_i
|
218
|
+
rescue ArgumentError
|
219
|
+
expires_at.to_i
|
178
220
|
end
|
179
221
|
end
|
180
222
|
end
|
data/lib/oauth2/authenticator.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'base64'
|
2
4
|
|
3
5
|
module OAuth2
|
@@ -35,7 +37,7 @@ module OAuth2
|
|
35
37
|
end
|
36
38
|
|
37
39
|
def self.encode_basic_auth(user, password)
|
38
|
-
|
40
|
+
"Basic #{Base64.strict_encode64("#{user}:#{password}")}"
|
39
41
|
end
|
40
42
|
|
41
43
|
private
|
@@ -43,13 +45,18 @@ module OAuth2
|
|
43
45
|
# Adds client_id and client_secret request parameters if they are not
|
44
46
|
# already set.
|
45
47
|
def apply_params_auth(params)
|
46
|
-
|
48
|
+
result = {}
|
49
|
+
result['client_id'] = id unless id.nil?
|
50
|
+
result['client_secret'] = secret unless secret.nil?
|
51
|
+
result.merge(params)
|
47
52
|
end
|
48
53
|
|
49
54
|
# When using schemes that don't require the client_secret to be passed i.e TLS Client Auth,
|
50
55
|
# we don't want to send the secret
|
51
56
|
def apply_client_id(params)
|
52
|
-
|
57
|
+
result = {}
|
58
|
+
result['client_id'] = id unless id.nil?
|
59
|
+
result.merge(params)
|
53
60
|
end
|
54
61
|
|
55
62
|
# Adds an `Authorization` header with Basic Auth credentials if and only if
|
@@ -57,10 +64,10 @@ module OAuth2
|
|
57
64
|
def apply_basic_auth(params)
|
58
65
|
headers = params.fetch(:headers, {})
|
59
66
|
headers = basic_auth_header.merge(headers)
|
60
|
-
params.merge(:
|
67
|
+
params.merge(headers: headers)
|
61
68
|
end
|
62
69
|
|
63
|
-
# @see https://
|
70
|
+
# @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
|
64
71
|
def basic_auth_header
|
65
72
|
{'Authorization' => self.class.encode_basic_auth(id, secret)}
|
66
73
|
end
|
data/lib/oauth2/client.rb
CHANGED
@@ -1,9 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'faraday'
|
2
4
|
require 'logger'
|
3
5
|
|
4
6
|
module OAuth2
|
7
|
+
ConnectionError = Class.new(Faraday::ConnectionFailed)
|
8
|
+
TimeoutError = Class.new(Faraday::TimeoutError)
|
9
|
+
|
5
10
|
# The OAuth2::Client class
|
6
11
|
class Client # rubocop:disable Metrics/ClassLength
|
12
|
+
RESERVED_PARAM_KEYS = %w[body headers params parse snaky].freeze
|
13
|
+
|
7
14
|
attr_reader :id, :secret, :site
|
8
15
|
attr_accessor :options
|
9
16
|
attr_writer :connection
|
@@ -14,17 +21,19 @@ module OAuth2
|
|
14
21
|
#
|
15
22
|
# @param [String] client_id the client_id value
|
16
23
|
# @param [String] client_secret the client_secret value
|
17
|
-
# @param [Hash]
|
18
|
-
# @option
|
19
|
-
# @option
|
20
|
-
# @option
|
21
|
-
# @option
|
22
|
-
# @option
|
23
|
-
# @option
|
24
|
-
# @option
|
25
|
-
# @option
|
26
|
-
# @option
|
27
|
-
#
|
24
|
+
# @param [Hash] options the options to create the client with
|
25
|
+
# @option options [String] :site the OAuth2 provider site host
|
26
|
+
# @option options [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange
|
27
|
+
# @option options [String] :authorize_url ('/oauth/authorize') absolute or relative URL path to the Authorization endpoint
|
28
|
+
# @option options [String] :token_url ('/oauth/token') absolute or relative URL path to the Token endpoint
|
29
|
+
# @option options [Symbol] :token_method (:post) HTTP method to use to request token (:get, :post, :post_with_query_string)
|
30
|
+
# @option options [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
|
31
|
+
# @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
|
32
|
+
# @option options [FixNum] :max_redirects (5) maximum number of redirects to follow
|
33
|
+
# @option options [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
|
34
|
+
# @option options [Logger] :logger (::Logger.new($stdout)) which logger to use when OAUTH_DEBUG is enabled
|
35
|
+
# @option options [Proc] :extract_access_token proc that takes the client and the response Hash and extracts the access token from the response (DEPRECATED)
|
36
|
+
# @option options [Class] :access_token_class [Class] class of access token for easier subclassing OAuth2::AccessToken, @version 2.0+
|
28
37
|
# @yield [builder] The Faraday connection builder
|
29
38
|
def initialize(client_id, client_secret, options = {}, &block)
|
30
39
|
opts = options.dup
|
@@ -32,20 +41,25 @@ module OAuth2
|
|
32
41
|
@secret = client_secret
|
33
42
|
@site = opts.delete(:site)
|
34
43
|
ssl = opts.delete(:ssl)
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
44
|
+
warn('OAuth2::Client#initialize argument `extract_access_token` will be removed in oauth2 v3. Refactor to use `access_token_class`.') if opts[:extract_access_token]
|
45
|
+
@options = {
|
46
|
+
authorize_url: 'oauth/authorize',
|
47
|
+
token_url: 'oauth/token',
|
48
|
+
token_method: :post,
|
49
|
+
auth_scheme: :basic_auth,
|
50
|
+
connection_opts: {},
|
51
|
+
connection_build: block,
|
52
|
+
max_redirects: 5,
|
53
|
+
raise_errors: true,
|
54
|
+
logger: ::Logger.new($stdout),
|
55
|
+
access_token_class: AccessToken,
|
56
|
+
}.merge(opts)
|
43
57
|
@options[:connection_opts][:ssl] = ssl if ssl
|
44
58
|
end
|
45
59
|
|
46
60
|
# Set the site host
|
47
61
|
#
|
48
|
-
# @param [String] the OAuth2 provider site host
|
62
|
+
# @param value [String] the OAuth2 provider site host
|
49
63
|
def site=(value)
|
50
64
|
@connection = nil
|
51
65
|
@site = value
|
@@ -53,15 +67,16 @@ module OAuth2
|
|
53
67
|
|
54
68
|
# The Faraday connection object
|
55
69
|
def connection
|
56
|
-
@connection ||=
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
options[:connection_build].call(
|
70
|
+
@connection ||=
|
71
|
+
Faraday.new(site, options[:connection_opts]) do |builder|
|
72
|
+
oauth_debug_logging(builder)
|
73
|
+
if options[:connection_build]
|
74
|
+
options[:connection_build].call(builder)
|
75
|
+
else
|
76
|
+
builder.request :url_encoded # form-encode POST params
|
77
|
+
builder.adapter Faraday.default_adapter # make requests with Net::HTTP
|
61
78
|
end
|
62
79
|
end
|
63
|
-
conn
|
64
|
-
end
|
65
80
|
end
|
66
81
|
|
67
82
|
# The authorize endpoint URL of the OAuth2 provider
|
@@ -80,6 +95,9 @@ module OAuth2
|
|
80
95
|
end
|
81
96
|
|
82
97
|
# Makes a request relative to the specified site root.
|
98
|
+
# Updated HTTP 1.1 specification (IETF RFC 7231) relaxed the original constraint (IETF RFC 2616),
|
99
|
+
# allowing the use of relative URLs in Location headers.
|
100
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2
|
83
101
|
#
|
84
102
|
# @param [Symbol] verb one of :get, :post, :put, :delete
|
85
103
|
# @param [String] url URL path of request
|
@@ -90,35 +108,36 @@ module OAuth2
|
|
90
108
|
# @option opts [Boolean] :raise_errors whether or not to raise an OAuth2::Error on 400+ status
|
91
109
|
# code response for this request. Will default to client option
|
92
110
|
# @option opts [Symbol] :parse @see Response::initialize
|
93
|
-
# @
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
url = connection.build_url(url).to_s
|
98
|
-
|
99
|
-
response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
|
100
|
-
req.params.update(opts[:params]) if opts[:params]
|
101
|
-
yield(req) if block_given?
|
102
|
-
end
|
103
|
-
response = Response.new(response, :parse => opts[:parse])
|
111
|
+
# @option opts [true, false] :snaky (true) @see Response::initialize
|
112
|
+
# @yield [req] @see Faraday::Connection#run_request
|
113
|
+
def request(verb, url, opts = {}, &block)
|
114
|
+
response = execute_request(verb, url, opts, &block)
|
104
115
|
|
105
116
|
case response.status
|
106
117
|
when 301, 302, 303, 307
|
107
118
|
opts[:redirect_count] ||= 0
|
108
119
|
opts[:redirect_count] += 1
|
109
120
|
return response if opts[:redirect_count] > options[:max_redirects]
|
121
|
+
|
110
122
|
if response.status == 303
|
111
123
|
verb = :get
|
112
124
|
opts.delete(:body)
|
113
125
|
end
|
114
|
-
|
126
|
+
location = response.headers['location']
|
127
|
+
if location
|
128
|
+
full_location = response.response.env.url.merge(location)
|
129
|
+
request(verb, full_location, opts)
|
130
|
+
else
|
131
|
+
error = Error.new(response)
|
132
|
+
raise(error, "Got #{response.status} status code, but no Location header was present")
|
133
|
+
end
|
115
134
|
when 200..299, 300..399
|
116
135
|
# on non-redirecting 3xx statuses, just return the response
|
117
136
|
response
|
118
137
|
when 400..599
|
119
138
|
error = Error.new(response)
|
120
139
|
raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
|
121
|
-
|
140
|
+
|
122
141
|
response
|
123
142
|
else
|
124
143
|
error = Error.new(response)
|
@@ -128,54 +147,84 @@ module OAuth2
|
|
128
147
|
|
129
148
|
# Initializes an AccessToken by making a request to the token endpoint
|
130
149
|
#
|
131
|
-
# @param [Hash]
|
132
|
-
#
|
133
|
-
#
|
150
|
+
# @param params [Hash] a Hash of params for the token endpoint, except:
|
151
|
+
# @option params [Symbol] :parse @see Response#initialize
|
152
|
+
# @option params [true, false] :snaky (true) @see Response#initialize
|
153
|
+
# @param access_token_opts [Hash] access token options, to pass to the AccessToken object
|
154
|
+
# @param extract_access_token [Proc] proc that extracts the access token from the response (DEPRECATED)
|
155
|
+
# @yield [req] @see Faraday::Connection#run_request
|
134
156
|
# @return [AccessToken] the initialized AccessToken
|
135
|
-
def get_token(params, access_token_opts = {},
|
136
|
-
|
137
|
-
|
138
|
-
headers = params
|
157
|
+
def get_token(params, access_token_opts = {}, extract_access_token = nil, &block)
|
158
|
+
warn('OAuth2::Client#get_token argument `extract_access_token` will be removed in oauth2 v3. Refactor to use `access_token_class` on #initialize.') if extract_access_token
|
159
|
+
extract_access_token ||= options[:extract_access_token]
|
160
|
+
parse, snaky, params, headers = parse_snaky_params_headers(params)
|
161
|
+
|
162
|
+
request_opts = {
|
163
|
+
raise_errors: options[:raise_errors],
|
164
|
+
parse: parse,
|
165
|
+
snaky: snaky,
|
166
|
+
}
|
139
167
|
if options[:token_method] == :post
|
140
|
-
|
141
|
-
|
168
|
+
|
169
|
+
# NOTE: If proliferation of request types continues we should implement a parser solution for Request,
|
170
|
+
# just like we have with Response.
|
171
|
+
request_opts[:body] = if headers['Content-Type'] == 'application/json'
|
172
|
+
params.to_json
|
173
|
+
else
|
174
|
+
params
|
175
|
+
end
|
176
|
+
|
177
|
+
request_opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
|
142
178
|
else
|
143
|
-
|
144
|
-
|
179
|
+
request_opts[:params] = params
|
180
|
+
request_opts[:headers] = {}
|
145
181
|
end
|
146
|
-
|
147
|
-
response = request(
|
148
|
-
|
149
|
-
|
150
|
-
|
182
|
+
request_opts[:headers].merge!(headers)
|
183
|
+
response = request(http_method, token_url, request_opts, &block)
|
184
|
+
|
185
|
+
# In v1.4.x, the deprecated extract_access_token option retrieves the token from the response.
|
186
|
+
# We preserve this behavior here, but a custom access_token_class that implements #from_hash
|
187
|
+
# should be used instead.
|
188
|
+
if extract_access_token
|
189
|
+
parse_response_legacy(response, access_token_opts, extract_access_token)
|
190
|
+
else
|
191
|
+
parse_response(response, access_token_opts)
|
151
192
|
end
|
152
|
-
|
193
|
+
end
|
194
|
+
|
195
|
+
# The HTTP Method of the request
|
196
|
+
# @return [Symbol] HTTP verb, one of :get, :post, :put, :delete
|
197
|
+
def http_method
|
198
|
+
http_meth = options[:token_method].to_sym
|
199
|
+
return :post if http_meth == :post_with_query_string
|
200
|
+
|
201
|
+
http_meth
|
153
202
|
end
|
154
203
|
|
155
204
|
# The Authorization Code strategy
|
156
205
|
#
|
157
|
-
# @see http://
|
206
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1
|
158
207
|
def auth_code
|
159
208
|
@auth_code ||= OAuth2::Strategy::AuthCode.new(self)
|
160
209
|
end
|
161
210
|
|
162
211
|
# The Implicit strategy
|
163
212
|
#
|
164
|
-
# @see http://
|
213
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2
|
165
214
|
def implicit
|
166
215
|
@implicit ||= OAuth2::Strategy::Implicit.new(self)
|
167
216
|
end
|
168
217
|
|
169
218
|
# The Resource Owner Password Credentials strategy
|
170
219
|
#
|
171
|
-
# @see http://
|
220
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3
|
172
221
|
def password
|
173
222
|
@password ||= OAuth2::Strategy::Password.new(self)
|
174
223
|
end
|
175
224
|
|
176
225
|
# The Client Credentials strategy
|
177
226
|
#
|
178
|
-
# @see http://
|
227
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4
|
179
228
|
def client_credentials
|
180
229
|
@client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self)
|
181
230
|
end
|
@@ -195,10 +244,10 @@ module OAuth2
|
|
195
244
|
#
|
196
245
|
# @api semipublic
|
197
246
|
#
|
198
|
-
# @see https://
|
199
|
-
# @see https://
|
200
|
-
# @see https://
|
201
|
-
# @see https://
|
247
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
|
248
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
|
249
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
|
250
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
|
202
251
|
# @return [Hash] the params to add to a request or URL
|
203
252
|
def redirection_params
|
204
253
|
if options[:redirect_uri]
|
@@ -207,5 +256,99 @@ module OAuth2
|
|
207
256
|
{}
|
208
257
|
end
|
209
258
|
end
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
def parse_snaky_params_headers(params)
|
263
|
+
params = params.map do |key, value|
|
264
|
+
if RESERVED_PARAM_KEYS.include?(key)
|
265
|
+
[key.to_sym, value]
|
266
|
+
else
|
267
|
+
[key, value]
|
268
|
+
end
|
269
|
+
end.to_h
|
270
|
+
parse = params.key?(:parse) ? params.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
|
271
|
+
snaky = params.key?(:snaky) ? params.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
|
272
|
+
params = authenticator.apply(params)
|
273
|
+
# authenticator may add :headers, and we remove them here
|
274
|
+
headers = params.delete(:headers) || {}
|
275
|
+
[parse, snaky, params, headers]
|
276
|
+
end
|
277
|
+
|
278
|
+
def execute_request(verb, url, opts = {})
|
279
|
+
url = connection.build_url(url).to_s
|
280
|
+
|
281
|
+
begin
|
282
|
+
response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
|
283
|
+
req.params.update(opts[:params]) if opts[:params]
|
284
|
+
yield(req) if block_given?
|
285
|
+
end
|
286
|
+
rescue Faraday::ConnectionFailed => e
|
287
|
+
raise ConnectionError, e
|
288
|
+
rescue Faraday::TimeoutError => e
|
289
|
+
raise TimeoutError, e
|
290
|
+
end
|
291
|
+
|
292
|
+
parse = opts.key?(:parse) ? opts.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
|
293
|
+
snaky = opts.key?(:snaky) ? opts.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
|
294
|
+
|
295
|
+
Response.new(response, parse: parse, snaky: snaky)
|
296
|
+
end
|
297
|
+
|
298
|
+
# Returns the authenticator object
|
299
|
+
#
|
300
|
+
# @return [Authenticator] the initialized Authenticator
|
301
|
+
def authenticator
|
302
|
+
Authenticator.new(id, secret, options[:auth_scheme])
|
303
|
+
end
|
304
|
+
|
305
|
+
def parse_response_legacy(response, access_token_opts, extract_access_token)
|
306
|
+
access_token = build_access_token_legacy(response, access_token_opts, extract_access_token)
|
307
|
+
|
308
|
+
return access_token if access_token
|
309
|
+
|
310
|
+
if options[:raise_errors]
|
311
|
+
error = Error.new(response)
|
312
|
+
raise(error)
|
313
|
+
end
|
314
|
+
|
315
|
+
nil
|
316
|
+
end
|
317
|
+
|
318
|
+
def parse_response(response, access_token_opts)
|
319
|
+
access_token_class = options[:access_token_class]
|
320
|
+
data = response.parsed
|
321
|
+
|
322
|
+
unless data.is_a?(Hash) && !data.empty?
|
323
|
+
return unless options[:raise_errors]
|
324
|
+
|
325
|
+
error = Error.new(response)
|
326
|
+
raise(error)
|
327
|
+
end
|
328
|
+
|
329
|
+
build_access_token(response, access_token_opts, access_token_class)
|
330
|
+
end
|
331
|
+
|
332
|
+
# Builds the access token from the response of the HTTP call
|
333
|
+
#
|
334
|
+
# @return [AccessToken] the initialized AccessToken
|
335
|
+
def build_access_token(response, access_token_opts, access_token_class)
|
336
|
+
access_token_class.from_hash(self, response.parsed.merge(access_token_opts)).tap do |access_token|
|
337
|
+
access_token.response = response if access_token.respond_to?(:response=)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# Builds the access token from the response of the HTTP call with legacy extract_access_token
|
342
|
+
#
|
343
|
+
# @return [AccessToken] the initialized AccessToken
|
344
|
+
def build_access_token_legacy(response, access_token_opts, extract_access_token)
|
345
|
+
extract_access_token.call(self, response.parsed.merge(access_token_opts))
|
346
|
+
rescue StandardError
|
347
|
+
nil
|
348
|
+
end
|
349
|
+
|
350
|
+
def oauth_debug_logging(builder)
|
351
|
+
builder.response :logger, options[:logger], bodies: true if ENV['OAUTH_DEBUG'] == 'true'
|
352
|
+
end
|
210
353
|
end
|
211
354
|
end
|