oauth2 1.4.7 → 2.0.9

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,57 +1,91 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
- class AccessToken
3
- attr_reader :client, :token, :expires_in, :expires_at, :params
4
- attr_accessor :options, :refresh_token
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
- # Should these methods be deprecated?
7
12
  class << self
8
13
  # Initializes an AccessToken from a Hash
9
14
  #
10
- # @param [Client] the OAuth2::Client instance
11
- # @param [Hash] a hash of AccessToken property values
12
- # @return [AccessToken] the initalized AccessToken
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
13
19
  def from_hash(client, hash)
14
- hash = hash.dup
15
- new(client, hash.delete('access_token') || hash.delete(:access_token), hash)
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)
16
26
  end
17
27
 
18
28
  # Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
19
29
  #
20
30
  # @param [Client] client the OAuth2::Client instance
21
31
  # @param [String] kvform the application/x-www-form-urlencoded string
22
- # @return [AccessToken] the initalized AccessToken
32
+ # @return [AccessToken] the initialized AccessToken
23
33
  def from_kvform(client, kvform)
24
34
  from_hash(client, Rack::Utils.parse_query(kvform))
25
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
26
46
  end
27
47
 
28
- # Initalize an AccessToken
48
+ # Initialize an AccessToken
29
49
  #
30
50
  # @param [Client] client the OAuth2::Client instance
31
- # @param [String] token the Access Token value
51
+ # @param [String] token the Access Token value (optional, may not be used in refresh flows)
32
52
  # @param [Hash] opts the options to create the Access Token with
33
53
  # @option opts [String] :refresh_token (nil) the refresh_token value
34
54
  # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
35
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+
36
57
  # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
37
58
  # one of :header, :body or :query
38
59
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
39
60
  # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
40
61
  # Access Token value in :body or :query transmission mode
41
- def initialize(client, token, opts = {}) # rubocop:disable Metrics/AbcSize
62
+ def initialize(client, token, opts = {})
42
63
  @client = client
43
64
  @token = token.to_s
65
+
44
66
  opts = opts.dup
45
- [:refresh_token, :expires_in, :expires_at].each do |arg|
67
+ %i[refresh_token expires_in expires_at expires_latency].each do |arg|
46
68
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
47
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
48
80
  @expires_in ||= opts.delete('expires')
49
81
  @expires_in &&= @expires_in.to_i
50
82
  @expires_at &&= convert_expires_at(@expires_at)
83
+ @expires_latency &&= @expires_latency.to_i
51
84
  @expires_at ||= Time.now.to_i + @expires_in if @expires_in
52
- @options = {:mode => opts.delete(:mode) || :header,
53
- :header_format => opts.delete(:header_format) || 'Bearer %s',
54
- :param_name => opts.delete(:param_name) || 'access_token'}
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'}
55
89
  @params = opts
56
90
  end
57
91
 
@@ -73,29 +107,36 @@ module OAuth2
73
107
  #
74
108
  # @return [Boolean]
75
109
  def expired?
76
- expires? && (expires_at < Time.now.to_i)
110
+ expires? && (expires_at <= Time.now.to_i)
77
111
  end
78
112
 
79
113
  # Refreshes the current Access Token
80
114
  #
81
115
  # @return [AccessToken] a new AccessToken
82
116
  # @note options should be carried over to the new AccessToken
83
- def refresh!(params = {})
117
+ def refresh(params = {}, access_token_opts = {})
84
118
  raise('A refresh_token is not available') unless refresh_token
85
119
 
86
120
  params[:grant_type] = 'refresh_token'
87
121
  params[:refresh_token] = refresh_token
88
- new_token = @client.get_token(params)
122
+ new_token = @client.get_token(params, access_token_opts)
89
123
  new_token.options = options
90
- new_token.refresh_token = refresh_token unless new_token.refresh_token
124
+ if new_token.refresh_token
125
+ # Keep it, if there is one
126
+ else
127
+ new_token.refresh_token = refresh_token
128
+ end
91
129
  new_token
92
130
  end
131
+ # A compatibility alias
132
+ # @note does not modify the receiver, so bang is not the default method
133
+ alias refresh! refresh
93
134
 
94
135
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
95
136
  #
96
137
  # @return [Hash] a hash of AccessToken property values
97
138
  def to_hash
98
- params.merge(:access_token => token, :refresh_token => refresh_token, :expires_at => expires_at)
139
+ params.merge(access_token: token, refresh_token: refresh_token, expires_at: expires_at)
99
140
  end
100
141
 
101
142
  # Make a request with the Access Token
@@ -103,7 +144,7 @@ module OAuth2
103
144
  # @param [Symbol] verb the HTTP request method
104
145
  # @param [String] path the HTTP URL path of the request
105
146
  # @param [Hash] opts the options to make the request with
106
- # @see Client#request
147
+ # @see Client#request
107
148
  def request(verb, path, opts = {}, &block)
108
149
  configure_authentication!(opts)
109
150
  @client.request(verb, path, opts, &block)
@@ -151,7 +192,7 @@ module OAuth2
151
192
 
152
193
  private
153
194
 
154
- def configure_authentication!(opts) # rubocop:disable Metrics/AbcSize
195
+ def configure_authentication!(opts)
155
196
  case options[:mode]
156
197
  when :header
157
198
  opts[:headers] ||= {}
@@ -164,7 +205,7 @@ module OAuth2
164
205
  if opts[:body].is_a?(Hash)
165
206
  opts[:body][options[:param_name]] = token
166
207
  else
167
- opts[:body] << "&#{options[:param_name]}=#{token}"
208
+ opts[:body] += "&#{options[:param_name]}=#{token}"
168
209
  end
169
210
  # @todo support for multi-part (file uploads)
170
211
  else
@@ -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
- 'Basic ' + Base64.encode64(user + ':' + password).delete("\n")
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
- {'client_id' => id, 'client_secret' => secret}.merge(params)
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
- {'client_id' => id}.merge(params)
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(:headers => headers)
67
+ params.merge(headers: headers)
61
68
  end
62
69
 
63
- # @see https://tools.ietf.org/html/rfc2617#section-2
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,10 +1,15 @@
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
7
- RESERVED_PARAM_KEYS = %w[headers parse].freeze
12
+ RESERVED_PARAM_KEYS = %w[body headers params parse snaky].freeze
8
13
 
9
14
  attr_reader :id, :secret, :site
10
15
  attr_accessor :options
@@ -16,17 +21,19 @@ module OAuth2
16
21
  #
17
22
  # @param [String] client_id the client_id value
18
23
  # @param [String] client_secret the client_secret value
19
- # @param [Hash] opts the options to create the client with
20
- # @option opts [String] :site the OAuth2 provider site host
21
- # @option opts [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange
22
- # @option opts [String] :authorize_url ('/oauth/authorize') absolute or relative URL path to the Authorization endpoint
23
- # @option opts [String] :token_url ('/oauth/token') absolute or relative URL path to the Token endpoint
24
- # @option opts [Symbol] :token_method (:post) HTTP method to use to request token (:get or :post)
25
- # @option opts [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
26
- # @option opts [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
27
- # @option opts [FixNum] :max_redirects (5) maximum number of redirects to follow
28
- # @option opts [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
29
- # @option opts [Proc] :extract_access_token proc that extracts the access token from the response
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+
30
37
  # @yield [builder] The Faraday connection builder
31
38
  def initialize(client_id, client_secret, options = {}, &block)
32
39
  opts = options.dup
@@ -34,24 +41,25 @@ module OAuth2
34
41
  @secret = client_secret
35
42
  @site = opts.delete(:site)
36
43
  ssl = opts.delete(:ssl)
37
-
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]
38
45
  @options = {
39
- :authorize_url => '/oauth/authorize',
40
- :token_url => '/oauth/token',
41
- :token_method => :post,
42
- :auth_scheme => :request_body,
43
- :connection_opts => {},
44
- :connection_build => block,
45
- :max_redirects => 5,
46
- :raise_errors => true,
47
- :extract_access_token => DEFAULT_EXTRACT_ACCESS_TOKEN,
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,
48
56
  }.merge(opts)
49
57
  @options[:connection_opts][:ssl] = ssl if ssl
50
58
  end
51
59
 
52
60
  # Set the site host
53
61
  #
54
- # @param [String] the OAuth2 provider site host
62
+ # @param value [String] the OAuth2 provider site host
55
63
  def site=(value)
56
64
  @connection = nil
57
65
  @site = value
@@ -59,15 +67,16 @@ module OAuth2
59
67
 
60
68
  # The Faraday connection object
61
69
  def connection
62
- @connection ||= begin
63
- conn = Faraday.new(site, options[:connection_opts])
64
- if options[:connection_build]
65
- conn.build do |b|
66
- options[:connection_build].call(b)
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
67
78
  end
68
79
  end
69
- conn
70
- end
71
80
  end
72
81
 
73
82
  # The authorize endpoint URL of the OAuth2 provider
@@ -86,6 +95,9 @@ module OAuth2
86
95
  end
87
96
 
88
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
89
101
  #
90
102
  # @param [Symbol] verb one of :get, :post, :put, :delete
91
103
  # @param [String] url URL path of request
@@ -96,17 +108,10 @@ module OAuth2
96
108
  # @option opts [Boolean] :raise_errors whether or not to raise an OAuth2::Error on 400+ status
97
109
  # code response for this request. Will default to client option
98
110
  # @option opts [Symbol] :parse @see Response::initialize
99
- # @yield [req] The Faraday request
100
- def request(verb, url, opts = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
101
- connection.response :logger, ::Logger.new($stdout) if ENV['OAUTH_DEBUG'] == 'true'
102
-
103
- url = connection.build_url(url).to_s
104
-
105
- response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
106
- req.params.update(opts[:params]) if opts[:params]
107
- yield(req) if block_given?
108
- end
109
- 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)
110
115
 
111
116
  case response.status
112
117
  when 301, 302, 303, 307
@@ -118,7 +123,14 @@ module OAuth2
118
123
  verb = :get
119
124
  opts.delete(:body)
120
125
  end
121
- request(verb, response.headers['location'], opts)
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
122
134
  when 200..299, 300..399
123
135
  # on non-redirecting 3xx statuses, just return the response
124
136
  response
@@ -126,7 +138,6 @@ module OAuth2
126
138
  error = Error.new(response)
127
139
  raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
128
140
 
129
- response.error = error
130
141
  response
131
142
  else
132
143
  error = Error.new(response)
@@ -136,70 +147,84 @@ module OAuth2
136
147
 
137
148
  # Initializes an AccessToken by making a request to the token endpoint
138
149
  #
139
- # @param [Hash] params a Hash of params for the token endpoint
140
- # @param [Hash] access token options, to pass to the AccessToken object
141
- # @param [Class] class of access token for easier subclassing OAuth2::AccessToken
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
142
156
  # @return [AccessToken] the initialized AccessToken
143
- def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token]) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
144
- params = params.map do |key, value|
145
- if RESERVED_PARAM_KEYS.include?(key)
146
- [key.to_sym, value]
147
- else
148
- [key, value]
149
- end
150
- end
151
- params = Hash[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)
152
161
 
153
- params = Authenticator.new(id, secret, options[:auth_scheme]).apply(params)
154
- opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
155
- headers = params.delete(:headers) || {}
162
+ request_opts = {
163
+ raise_errors: options[:raise_errors],
164
+ parse: parse,
165
+ snaky: snaky,
166
+ }
156
167
  if options[:token_method] == :post
157
- opts[:body] = params
158
- opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
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'}
159
178
  else
160
- opts[:params] = params
161
- opts[:headers] = {}
179
+ request_opts[:params] = params
180
+ request_opts[:headers] = {}
162
181
  end
163
- opts[:headers].merge!(headers)
164
- response = request(options[:token_method], token_url, opts)
182
+ request_opts[:headers].merge!(headers)
183
+ response = request(http_method, token_url, request_opts, &block)
165
184
 
166
- access_token = begin
167
- build_access_token(response, access_token_opts, extract_access_token)
168
- rescue StandardError
169
- nil
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)
170
192
  end
193
+ end
171
194
 
172
- if options[:raise_errors] && !access_token
173
- error = Error.new(response)
174
- raise(error)
175
- end
176
- access_token
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
177
202
  end
178
203
 
179
204
  # The Authorization Code strategy
180
205
  #
181
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
206
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1
182
207
  def auth_code
183
208
  @auth_code ||= OAuth2::Strategy::AuthCode.new(self)
184
209
  end
185
210
 
186
211
  # The Implicit strategy
187
212
  #
188
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2
213
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2
189
214
  def implicit
190
215
  @implicit ||= OAuth2::Strategy::Implicit.new(self)
191
216
  end
192
217
 
193
218
  # The Resource Owner Password Credentials strategy
194
219
  #
195
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.3
220
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3
196
221
  def password
197
222
  @password ||= OAuth2::Strategy::Password.new(self)
198
223
  end
199
224
 
200
225
  # The Client Credentials strategy
201
226
  #
202
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.4
227
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4
203
228
  def client_credentials
204
229
  @client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self)
205
230
  end
@@ -219,10 +244,10 @@ module OAuth2
219
244
  #
220
245
  # @api semipublic
221
246
  #
222
- # @see https://tools.ietf.org/html/rfc6749#section-4.1
223
- # @see https://tools.ietf.org/html/rfc6749#section-4.1.3
224
- # @see https://tools.ietf.org/html/rfc6749#section-4.2.1
225
- # @see https://tools.ietf.org/html/rfc6749#section-10.6
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
226
251
  # @return [Hash] the params to add to a request or URL
227
252
  def redirection_params
228
253
  if options[:redirect_uri]
@@ -232,26 +257,98 @@ module OAuth2
232
257
  end
233
258
  end
234
259
 
235
- DEFAULT_EXTRACT_ACCESS_TOKEN = proc do |client, hash|
236
- token = hash.delete('access_token') || hash.delete(:access_token)
237
- token && AccessToken.new(client, token, hash)
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]
238
276
  end
239
277
 
240
- private
278
+ def execute_request(verb, url, opts = {})
279
+ url = connection.build_url(url).to_s
241
280
 
242
- def build_access_token(response, access_token_opts, extract_access_token)
243
- parsed_response = response.parsed.dup
244
- return unless parsed_response.is_a?(Hash)
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
245
291
 
246
- hash = parsed_response.merge(access_token_opts)
292
+ parse = opts.key?(:parse) ? opts.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
293
+ snaky = opts.key?(:snaky) ? opts.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
247
294
 
248
- # Provide backwards compatibility for old AcessToken.form_hash pattern
249
- # Should be deprecated in 2.x
250
- if extract_access_token.is_a?(Class) && extract_access_token.respond_to?(:from_hash)
251
- extract_access_token.from_hash(self, hash)
252
- else
253
- extract_access_token.call(self, hash)
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)
254
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'
255
352
  end
256
353
  end
257
354
  end