oauth2 1.4.7 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/SECURITY.md ADDED
@@ -0,0 +1,20 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |----------|---------------------------|
7
+ | 2.latest | ✅ |
8
+ | 1.latest | ✅ (security updates only) |
9
+ | older | ⛔️ |
10
+
11
+ ## Reporting a Vulnerability
12
+
13
+ To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
14
+ Tidelift will coordinate the fix and disclosure.
15
+
16
+ ## OAuth2 for Enterprise
17
+
18
+ Available as part of the Tidelift Subscription.
19
+
20
+ 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.](https://tidelift.com/subscription/pkg/rubygems-oauth2?utm_source=rubygems-oauth2&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
@@ -1,31 +1,36 @@
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
+ attr_reader :client, :token, :expires_in, :expires_at, :expires_latency, :params
6
+ attr_accessor :options, :refresh_token, :response
5
7
 
6
- # Should these methods be deprecated?
7
8
  class << self
8
9
  # Initializes an AccessToken from a Hash
9
10
  #
10
- # @param [Client] the OAuth2::Client instance
11
- # @param [Hash] a hash of AccessToken property values
12
- # @return [AccessToken] the initalized AccessToken
11
+ # @param client [Client] the OAuth2::Client instance
12
+ # @param hash [Hash] a hash of AccessToken property values
13
+ # @return [AccessToken] the initialized AccessToken
13
14
  def from_hash(client, hash)
14
15
  hash = hash.dup
15
- new(client, hash.delete('access_token') || hash.delete(:access_token), hash)
16
+ new(client, hash.delete('access_token') || hash.delete(:access_token) || hash.delete('token') || hash.delete(:token), hash)
16
17
  end
17
18
 
18
19
  # Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
19
20
  #
20
21
  # @param [Client] client the OAuth2::Client instance
21
22
  # @param [String] kvform the application/x-www-form-urlencoded string
22
- # @return [AccessToken] the initalized AccessToken
23
+ # @return [AccessToken] the initialized AccessToken
23
24
  def from_kvform(client, kvform)
24
25
  from_hash(client, Rack::Utils.parse_query(kvform))
25
26
  end
27
+
28
+ def contains_token?(hash)
29
+ hash.key?('access_token') || hash.key?('id_token') || hash.key?('token')
30
+ end
26
31
  end
27
32
 
28
- # Initalize an AccessToken
33
+ # Initialize an AccessToken
29
34
  #
30
35
  # @param [Client] client the OAuth2::Client instance
31
36
  # @param [String] token the Access Token value
@@ -33,25 +38,28 @@ module OAuth2
33
38
  # @option opts [String] :refresh_token (nil) the refresh_token value
34
39
  # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
35
40
  # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
41
+ # @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
42
  # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
37
43
  # one of :header, :body or :query
38
44
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
39
45
  # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
40
46
  # Access Token value in :body or :query transmission mode
41
- def initialize(client, token, opts = {}) # rubocop:disable Metrics/AbcSize
47
+ def initialize(client, token, opts = {})
42
48
  @client = client
43
49
  @token = token.to_s
44
50
  opts = opts.dup
45
- [:refresh_token, :expires_in, :expires_at].each do |arg|
51
+ %i[refresh_token expires_in expires_at expires_latency].each do |arg|
46
52
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
47
53
  end
48
54
  @expires_in ||= opts.delete('expires')
49
55
  @expires_in &&= @expires_in.to_i
50
56
  @expires_at &&= convert_expires_at(@expires_at)
57
+ @expires_latency &&= @expires_latency.to_i
51
58
  @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'}
59
+ @expires_at -= @expires_latency if @expires_latency
60
+ @options = {mode: opts.delete(:mode) || :header,
61
+ header_format: opts.delete(:header_format) || 'Bearer %s',
62
+ param_name: opts.delete(:param_name) || 'access_token'}
55
63
  @params = opts
56
64
  end
57
65
 
@@ -73,29 +81,32 @@ module OAuth2
73
81
  #
74
82
  # @return [Boolean]
75
83
  def expired?
76
- expires? && (expires_at < Time.now.to_i)
84
+ expires? && (expires_at <= Time.now.to_i)
77
85
  end
78
86
 
79
87
  # Refreshes the current Access Token
80
88
  #
81
89
  # @return [AccessToken] a new AccessToken
82
90
  # @note options should be carried over to the new AccessToken
83
- def refresh!(params = {})
91
+ def refresh(params = {}, access_token_opts = {})
84
92
  raise('A refresh_token is not available') unless refresh_token
85
93
 
86
94
  params[:grant_type] = 'refresh_token'
87
95
  params[:refresh_token] = refresh_token
88
- new_token = @client.get_token(params)
96
+ new_token = @client.get_token(params, access_token_opts)
89
97
  new_token.options = options
90
98
  new_token.refresh_token = refresh_token unless new_token.refresh_token
91
99
  new_token
92
100
  end
101
+ # A compatibility alias
102
+ # @note does not modify the receiver, so bang is not the default method
103
+ alias refresh! refresh
93
104
 
94
105
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
95
106
  #
96
107
  # @return [Hash] a hash of AccessToken property values
97
108
  def to_hash
98
- params.merge(:access_token => token, :refresh_token => refresh_token, :expires_at => expires_at)
109
+ params.merge(access_token: token, refresh_token: refresh_token, expires_at: expires_at)
99
110
  end
100
111
 
101
112
  # Make a request with the Access Token
@@ -103,7 +114,7 @@ module OAuth2
103
114
  # @param [Symbol] verb the HTTP request method
104
115
  # @param [String] path the HTTP URL path of the request
105
116
  # @param [Hash] opts the options to make the request with
106
- # @see Client#request
117
+ # @see Client#request
107
118
  def request(verb, path, opts = {}, &block)
108
119
  configure_authentication!(opts)
109
120
  @client.request(verb, path, opts, &block)
@@ -151,7 +162,7 @@ module OAuth2
151
162
 
152
163
  private
153
164
 
154
- def configure_authentication!(opts) # rubocop:disable Metrics/AbcSize
165
+ def configure_authentication!(opts)
155
166
  case options[:mode]
156
167
  when :header
157
168
  opts[:headers] ||= {}
@@ -164,7 +175,7 @@ module OAuth2
164
175
  if opts[:body].is_a?(Hash)
165
176
  opts[:body][options[:param_name]] = token
166
177
  else
167
- opts[:body] << "&#{options[:param_name]}=#{token}"
178
+ opts[:body] += "&#{options[:param_name]}=#{token}"
168
179
  end
169
180
  # @todo support for multi-part (file uploads)
170
181
  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,7 +1,12 @@
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
12
  RESERVED_PARAM_KEYS = %w[headers parse].freeze
@@ -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 [Symbol] :snaky @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,78 @@ 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 @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
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]
144
160
  params = params.map do |key, value|
145
161
  if RESERVED_PARAM_KEYS.include?(key)
146
162
  [key.to_sym, value]
147
163
  else
148
164
  [key, value]
149
165
  end
150
- end
151
- params = Hash[params]
166
+ end.to_h
167
+
168
+ request_opts = {
169
+ raise_errors: options[:raise_errors],
170
+ parse: params.delete(:parse),
171
+ snaky: params.delete(:snaky),
172
+ }
152
173
 
153
- params = Authenticator.new(id, secret, options[:auth_scheme]).apply(params)
154
- opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
174
+ params = authenticator.apply(params)
155
175
  headers = params.delete(:headers) || {}
156
176
  if options[:token_method] == :post
157
- opts[:body] = params
158
- opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
177
+ request_opts[:body] = params
178
+ request_opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
159
179
  else
160
- opts[:params] = params
161
- opts[:headers] = {}
180
+ request_opts[:params] = params
181
+ request_opts[:headers] = {}
162
182
  end
163
- opts[:headers].merge!(headers)
164
- response = request(options[:token_method], token_url, opts)
183
+ request_opts[:headers].merge!(headers)
184
+ http_method = options[:token_method]
185
+ http_method = :post if http_method == :post_with_query_string
186
+ response = request(http_method, token_url, request_opts, &block)
165
187
 
166
- access_token = begin
167
- build_access_token(response, access_token_opts, extract_access_token)
168
- rescue StandardError
169
- nil
170
- end
171
-
172
- if options[:raise_errors] && !access_token
173
- error = Error.new(response)
174
- raise(error)
188
+ # In v1.4.x, the deprecated extract_access_token option retrieves the token from the response.
189
+ # We preserve this behavior here, but a custom access_token_class that implements #from_hash
190
+ # should be used instead.
191
+ if extract_access_token
192
+ parse_response_with_legacy_extract(response, access_token_opts, extract_access_token)
193
+ else
194
+ parse_response(response, access_token_opts)
175
195
  end
176
- access_token
177
196
  end
178
197
 
179
198
  # The Authorization Code strategy
180
199
  #
181
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
200
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1
182
201
  def auth_code
183
202
  @auth_code ||= OAuth2::Strategy::AuthCode.new(self)
184
203
  end
185
204
 
186
205
  # The Implicit strategy
187
206
  #
188
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2
207
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2
189
208
  def implicit
190
209
  @implicit ||= OAuth2::Strategy::Implicit.new(self)
191
210
  end
192
211
 
193
212
  # The Resource Owner Password Credentials strategy
194
213
  #
195
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.3
214
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3
196
215
  def password
197
216
  @password ||= OAuth2::Strategy::Password.new(self)
198
217
  end
199
218
 
200
219
  # The Client Credentials strategy
201
220
  #
202
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.4
221
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4
203
222
  def client_credentials
204
223
  @client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self)
205
224
  end
@@ -219,10 +238,10 @@ module OAuth2
219
238
  #
220
239
  # @api semipublic
221
240
  #
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
241
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
242
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
243
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
244
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
226
245
  # @return [Hash] the params to add to a request or URL
227
246
  def redirection_params
228
247
  if options[:redirect_uri]
@@ -232,26 +251,79 @@ module OAuth2
232
251
  end
233
252
  end
234
253
 
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)
254
+ private
255
+
256
+ def execute_request(verb, url, opts = {})
257
+ url = connection.build_url(url).to_s
258
+
259
+ begin
260
+ response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
261
+ req.params.update(opts[:params]) if opts[:params]
262
+ yield(req) if block_given?
263
+ end
264
+ rescue Faraday::ConnectionFailed => e
265
+ raise ConnectionError, e
266
+ rescue Faraday::TimeoutError => e
267
+ raise TimeoutError, e
268
+ end
269
+
270
+ Response.new(response, parse: opts[:parse], snaky: opts[:snaky])
238
271
  end
239
272
 
240
- private
273
+ # Returns the authenticator object
274
+ #
275
+ # @return [Authenticator] the initialized Authenticator
276
+ def authenticator
277
+ Authenticator.new(id, secret, options[:auth_scheme])
278
+ end
241
279
 
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)
280
+ def parse_response_with_legacy_extract(response, access_token_opts, extract_access_token)
281
+ access_token = build_access_token_legacy_extract(response, access_token_opts, extract_access_token)
245
282
 
246
- hash = parsed_response.merge(access_token_opts)
283
+ return access_token if access_token
247
284
 
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)
285
+ if options[:raise_errors]
286
+ error = Error.new(response)
287
+ raise(error)
288
+ end
289
+
290
+ nil
291
+ end
292
+
293
+ def parse_response(response, access_token_opts)
294
+ access_token_class = options[:access_token_class]
295
+ data = response.parsed
296
+
297
+ unless data.is_a?(Hash) && access_token_class.contains_token?(data)
298
+ return unless options[:raise_errors]
299
+
300
+ error = Error.new(response)
301
+ raise(error)
254
302
  end
303
+
304
+ build_access_token(response, access_token_opts, access_token_class)
305
+ end
306
+
307
+ # Builds the access token from the response of the HTTP call
308
+ #
309
+ # @return [AccessToken] the initialized AccessToken
310
+ def build_access_token(response, access_token_opts, access_token_class)
311
+ access_token_class.from_hash(self, response.parsed.merge(access_token_opts)).tap do |access_token|
312
+ access_token.response = response if access_token.respond_to?(:response=)
313
+ end
314
+ end
315
+
316
+ # Builds the access token from the response of the HTTP call with legacy extract_access_token
317
+ #
318
+ # @return [AccessToken] the initialized AccessToken
319
+ def build_access_token_legacy_extract(response, access_token_opts, extract_access_token)
320
+ extract_access_token.call(self, response.parsed.merge(access_token_opts))
321
+ rescue StandardError
322
+ nil
323
+ end
324
+
325
+ def oauth_debug_logging(builder)
326
+ builder.response :logger, options[:logger], bodies: true if ENV['OAUTH_DEBUG'] == 'true'
255
327
  end
256
328
  end
257
329
  end
data/lib/oauth2/error.rb CHANGED
@@ -1,40 +1,51 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
4
  class Error < StandardError
3
5
  attr_reader :response, :code, :description
4
6
 
5
- # standard error values include:
6
- # :invalid_request, :invalid_client, :invalid_token, :invalid_grant, :unsupported_grant_type, :invalid_scope
7
+ # standard error codes include:
8
+ # 'invalid_request', 'invalid_client', 'invalid_token', 'invalid_grant', 'unsupported_grant_type', 'invalid_scope'
7
9
  def initialize(response)
8
- response.error = self
9
10
  @response = response
11
+ message_opts = {}
10
12
 
11
13
  if response.parsed.is_a?(Hash)
12
14
  @code = response.parsed['error']
13
15
  @description = response.parsed['error_description']
14
- error_description = "#{@code}: #{@description}"
16
+ message_opts = parse_error_description(@code, @description)
15
17
  end
16
18
 
17
- super(error_message(response.body, :error_description => error_description))
19
+ super(error_message(response.body, message_opts))
18
20
  end
19
21
 
20
- # Makes a error message
21
- # @param [String] response_body response body of request
22
- # @param [String] opts :error_description error description to show first line
22
+ private
23
+
23
24
  def error_message(response_body, opts = {})
24
- message = []
25
+ lines = []
26
+
27
+ lines << opts[:error_description] if opts[:error_description]
25
28
 
26
- opts[:error_description] && message << opts[:error_description]
29
+ error_string = if response_body.respond_to?(:encode) && opts[:error_description].respond_to?(:encoding)
30
+ script_encoding = opts[:error_description].encoding
31
+ response_body.encode(script_encoding, invalid: :replace, undef: :replace)
32
+ else
33
+ response_body
34
+ end
35
+
36
+ lines << error_string
37
+
38
+ lines.join("\n")
39
+ end
27
40
 
28
- error_message = if opts[:error_description] && opts[:error_description].respond_to?(:encoding)
29
- script_encoding = opts[:error_description].encoding
30
- response_body.encode(script_encoding, :invalid => :replace, :undef => :replace)
31
- else
32
- response_body
33
- end
41
+ def parse_error_description(code, description)
42
+ return {} unless code || description
34
43
 
35
- message << error_message
44
+ error_description = ''
45
+ error_description += "#{code}: " if code
46
+ error_description += description if description
36
47
 
37
- message.join("\n")
48
+ {error_description: error_description}
38
49
  end
39
50
  end
40
51
  end