oauth2 1.4.0 → 2.0.0

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.
@@ -1,30 +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
8
  class << self
7
9
  # Initializes an AccessToken from a Hash
8
10
  #
9
- # @param [Client] the OAuth2::Client instance
10
- # @param [Hash] a hash of AccessToken property values
11
- # @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
12
14
  def from_hash(client, hash)
13
15
  hash = hash.dup
14
- 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)
15
17
  end
16
18
 
17
19
  # Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
18
20
  #
19
21
  # @param [Client] client the OAuth2::Client instance
20
22
  # @param [String] kvform the application/x-www-form-urlencoded string
21
- # @return [AccessToken] the initalized AccessToken
23
+ # @return [AccessToken] the initialized AccessToken
22
24
  def from_kvform(client, kvform)
23
25
  from_hash(client, Rack::Utils.parse_query(kvform))
24
26
  end
27
+
28
+ def contains_token?(hash)
29
+ hash.key?('access_token') || hash.key?('id_token') || hash.key?('token')
30
+ end
25
31
  end
26
32
 
27
- # Initalize an AccessToken
33
+ # Initialize an AccessToken
28
34
  #
29
35
  # @param [Client] client the OAuth2::Client instance
30
36
  # @param [String] token the Access Token value
@@ -32,25 +38,28 @@ module OAuth2
32
38
  # @option opts [String] :refresh_token (nil) the refresh_token value
33
39
  # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
34
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+
35
42
  # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
36
43
  # one of :header, :body or :query
37
44
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
38
45
  # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
39
46
  # Access Token value in :body or :query transmission mode
40
- def initialize(client, token, opts = {}) # rubocop:disable Metrics/AbcSize
47
+ def initialize(client, token, opts = {})
41
48
  @client = client
42
49
  @token = token.to_s
43
50
  opts = opts.dup
44
- [:refresh_token, :expires_in, :expires_at].each do |arg|
51
+ %i[refresh_token expires_in expires_at expires_latency].each do |arg|
45
52
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
46
53
  end
47
54
  @expires_in ||= opts.delete('expires')
48
55
  @expires_in &&= @expires_in.to_i
49
- @expires_at &&= @expires_at.to_i
56
+ @expires_at &&= convert_expires_at(@expires_at)
57
+ @expires_latency &&= @expires_latency.to_i
50
58
  @expires_at ||= Time.now.to_i + @expires_in if @expires_in
51
- @options = {:mode => opts.delete(:mode) || :header,
52
- :header_format => opts.delete(:header_format) || 'Bearer %s',
53
- :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'}
54
63
  @params = opts
55
64
  end
56
65
 
@@ -72,28 +81,32 @@ module OAuth2
72
81
  #
73
82
  # @return [Boolean]
74
83
  def expired?
75
- expires? && (expires_at < Time.now.to_i)
84
+ expires? && (expires_at <= Time.now.to_i)
76
85
  end
77
86
 
78
87
  # Refreshes the current Access Token
79
88
  #
80
89
  # @return [AccessToken] a new AccessToken
81
90
  # @note options should be carried over to the new AccessToken
82
- def refresh!(params = {})
91
+ def refresh(params = {}, access_token_opts = {}, access_token_class: self.class)
83
92
  raise('A refresh_token is not available') unless refresh_token
93
+
84
94
  params[:grant_type] = 'refresh_token'
85
95
  params[:refresh_token] = refresh_token
86
- new_token = @client.get_token(params)
96
+ new_token = @client.get_token(params, access_token_opts, access_token_class: access_token_class)
87
97
  new_token.options = options
88
98
  new_token.refresh_token = refresh_token unless new_token.refresh_token
89
99
  new_token
90
100
  end
101
+ # A compatibility alias
102
+ # @note does not modify the receiver, so bang is not the default method
103
+ alias refresh! refresh
91
104
 
92
105
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
93
106
  #
94
107
  # @return [Hash] a hash of AccessToken property values
95
108
  def to_hash
96
- 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)
97
110
  end
98
111
 
99
112
  # Make a request with the Access Token
@@ -149,7 +162,7 @@ module OAuth2
149
162
 
150
163
  private
151
164
 
152
- def configure_authentication!(opts) # rubocop:disable MethodLength, Metrics/AbcSize
165
+ def configure_authentication!(opts)
153
166
  case options[:mode]
154
167
  when :header
155
168
  opts[:headers] ||= {}
@@ -162,12 +175,18 @@ module OAuth2
162
175
  if opts[:body].is_a?(Hash)
163
176
  opts[:body][options[:param_name]] = token
164
177
  else
165
- opts[:body] << "&#{options[:param_name]}=#{token}"
178
+ opts[:body] += "&#{options[:param_name]}=#{token}"
166
179
  end
167
180
  # @todo support for multi-part (file uploads)
168
181
  else
169
182
  raise("invalid :mode option of #{options[:mode]}")
170
183
  end
171
184
  end
185
+
186
+ def convert_expires_at(expires_at)
187
+ Time.iso8601(expires_at.to_s).to_i
188
+ rescue ArgumentError
189
+ expires_at.to_i
190
+ end
172
191
  end
173
192
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
 
3
5
  module OAuth2
@@ -25,13 +27,17 @@ module OAuth2
25
27
  apply_basic_auth(params)
26
28
  when :request_body
27
29
  apply_params_auth(params)
30
+ when :tls_client_auth
31
+ apply_client_id(params)
32
+ when :private_key_jwt
33
+ params
28
34
  else
29
35
  raise NotImplementedError
30
36
  end
31
37
  end
32
38
 
33
39
  def self.encode_basic_auth(user, password)
34
- 'Basic ' + Base64.encode64(user + ':' + password).delete("\n")
40
+ "Basic #{Base64.strict_encode64("#{user}:#{password}")}"
35
41
  end
36
42
 
37
43
  private
@@ -39,7 +45,18 @@ module OAuth2
39
45
  # Adds client_id and client_secret request parameters if they are not
40
46
  # already set.
41
47
  def apply_params_auth(params)
42
- {'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)
52
+ end
53
+
54
+ # When using schemes that don't require the client_secret to be passed i.e TLS Client Auth,
55
+ # we don't want to send the secret
56
+ def apply_client_id(params)
57
+ result = {}
58
+ result['client_id'] = id unless id.nil?
59
+ result.merge(params)
43
60
  end
44
61
 
45
62
  # Adds an `Authorization` header with Basic Auth credentials if and only if
@@ -47,10 +64,10 @@ module OAuth2
47
64
  def apply_basic_auth(params)
48
65
  headers = params.fetch(:headers, {})
49
66
  headers = basic_auth_header.merge(headers)
50
- params.merge(:headers => headers)
67
+ params.merge(headers: headers)
51
68
  end
52
69
 
53
- # @see https://tools.ietf.org/html/rfc2617#section-2
70
+ # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
54
71
  def basic_auth_header
55
72
  {'Authorization' => self.class.encode_basic_auth(id, secret)}
56
73
  end
data/lib/oauth2/client.rb CHANGED
@@ -1,9 +1,14 @@
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)
5
8
  # The OAuth2::Client class
6
9
  class Client # rubocop:disable Metrics/ClassLength
10
+ RESERVED_PARAM_KEYS = %w[headers parse].freeze
11
+
7
12
  attr_reader :id, :secret, :site
8
13
  attr_accessor :options
9
14
  attr_writer :connection
@@ -14,17 +19,18 @@ module OAuth2
14
19
  #
15
20
  # @param [String] client_id the client_id value
16
21
  # @param [String] client_secret the client_secret value
17
- # @param [Hash] opts the options to create the client with
18
- # @option opts [String] :site the OAuth2 provider site host
19
- # @option opts [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange
20
- # @option opts [String] :authorize_url ('/oauth/authorize') absolute or relative URL path to the Authorization endpoint
21
- # @option opts [String] :token_url ('/oauth/token') absolute or relative URL path to the Token endpoint
22
- # @option opts [Symbol] :token_method (:post) HTTP method to use to request token (:get or :post)
23
- # @option opts [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
24
- # @option opts [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
25
- # @option opts [FixNum] :max_redirects (5) maximum number of redirects to follow
26
- # @option opts [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error
27
- # on responses with 400+ status codes
22
+ # @param [Hash] options the options to create the client with
23
+ # @option options [String] :site the OAuth2 provider site host
24
+ # @option options [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange
25
+ # @option options [String] :authorize_url ('/oauth/authorize') absolute or relative URL path to the Authorization endpoint
26
+ # @option options [String] :token_url ('/oauth/token') absolute or relative URL path to the Token endpoint
27
+ # @option options [Symbol] :token_method (:post) HTTP method to use to request token (:get, :post, :post_with_query_string)
28
+ # @option options [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
29
+ # @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
30
+ # @option options [FixNum] :max_redirects (5) maximum number of redirects to follow
31
+ # @option options [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
32
+ # @option options [Logger] :logger (::Logger.new($stdout)) which logger to use when OAUTH_DEBUG is enabled
33
+ # @option options [Proc] :extract_access_token proc that takes the client and the response Hash and extracts the access token from the response (DEPRECATED)
28
34
  # @yield [builder] The Faraday connection builder
29
35
  def initialize(client_id, client_secret, options = {}, &block)
30
36
  opts = options.dup
@@ -32,20 +38,24 @@ module OAuth2
32
38
  @secret = client_secret
33
39
  @site = opts.delete(:site)
34
40
  ssl = opts.delete(:ssl)
35
- @options = {:authorize_url => '/oauth/authorize',
36
- :token_url => '/oauth/token',
37
- :token_method => :post,
38
- :auth_scheme => :request_body,
39
- :connection_opts => {},
40
- :connection_build => block,
41
- :max_redirects => 5,
42
- :raise_errors => true}.merge(opts)
41
+
42
+ @options = {
43
+ authorize_url: 'oauth/authorize',
44
+ token_url: 'oauth/token',
45
+ token_method: :post,
46
+ auth_scheme: :basic_auth,
47
+ connection_opts: {},
48
+ connection_build: block,
49
+ max_redirects: 5,
50
+ raise_errors: true,
51
+ logger: ::Logger.new($stdout),
52
+ }.merge(opts)
43
53
  @options[:connection_opts][:ssl] = ssl if ssl
44
54
  end
45
55
 
46
56
  # Set the site host
47
57
  #
48
- # @param [String] the OAuth2 provider site host
58
+ # @param value [String] the OAuth2 provider site host
49
59
  def site=(value)
50
60
  @connection = nil
51
61
  @site = value
@@ -53,15 +63,16 @@ module OAuth2
53
63
 
54
64
  # The Faraday connection object
55
65
  def connection
56
- @connection ||= begin
57
- conn = Faraday.new(site, options[:connection_opts])
58
- if options[:connection_build]
59
- conn.build do |b|
60
- options[:connection_build].call(b)
66
+ @connection ||=
67
+ Faraday.new(site, options[:connection_opts]) do |builder|
68
+ oauth_debug_logging(builder)
69
+ if options[:connection_build]
70
+ options[:connection_build].call(builder)
71
+ else
72
+ builder.request :url_encoded # form-encode POST params
73
+ builder.adapter Faraday.default_adapter # make requests with Net::HTTP
61
74
  end
62
75
  end
63
- conn
64
- end
65
76
  end
66
77
 
67
78
  # The authorize endpoint URL of the OAuth2 provider
@@ -80,6 +91,9 @@ module OAuth2
80
91
  end
81
92
 
82
93
  # Makes a request relative to the specified site root.
94
+ # Updated HTTP 1.1 specification (IETF RFC 7231) relaxed the original constraint (IETF RFC 2616),
95
+ # allowing the use of relative URLs in Location headers.
96
+ # @see https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2
83
97
  #
84
98
  # @param [Symbol] verb one of :get, :post, :put, :delete
85
99
  # @param [String] url URL path of request
@@ -91,33 +105,45 @@ module OAuth2
91
105
  # code response for this request. Will default to client option
92
106
  # @option opts [Symbol] :parse @see Response::initialize
93
107
  # @yield [req] The Faraday request
94
- def request(verb, url, opts = {}) # rubocop:disable CyclomaticComplexity, MethodLength, Metrics/AbcSize
95
- connection.response :logger, ::Logger.new($stdout) if ENV['OAUTH_DEBUG'] == 'true'
96
-
97
- url = connection.build_url(url, opts[:params]).to_s
108
+ def request(verb, url, opts = {})
109
+ url = connection.build_url(url).to_s
98
110
 
99
- response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
100
- yield(req) if block_given?
111
+ begin
112
+ response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
113
+ req.params.update(opts[:params]) if opts[:params]
114
+ yield(req) if block_given?
115
+ end
116
+ rescue Faraday::ConnectionFailed => e
117
+ raise ConnectionError, e
101
118
  end
102
- response = Response.new(response, :parse => opts[:parse])
119
+
120
+ response = Response.new(response, parse: opts[:parse])
103
121
 
104
122
  case response.status
105
123
  when 301, 302, 303, 307
106
124
  opts[:redirect_count] ||= 0
107
125
  opts[:redirect_count] += 1
108
126
  return response if opts[:redirect_count] > options[:max_redirects]
127
+
109
128
  if response.status == 303
110
129
  verb = :get
111
130
  opts.delete(:body)
112
131
  end
113
- request(verb, response.headers['location'], opts)
132
+ location = response.headers['location']
133
+ if location
134
+ full_location = response.response.env.url.merge(location)
135
+ request(verb, full_location, opts)
136
+ else
137
+ error = Error.new(response)
138
+ raise(error, "Got #{response.status} status code, but no Location header was present")
139
+ end
114
140
  when 200..299, 300..399
115
141
  # on non-redirecting 3xx statuses, just return the response
116
142
  response
117
143
  when 400..599
118
144
  error = Error.new(response)
119
145
  raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
120
- response.error = error
146
+
121
147
  response
122
148
  else
123
149
  error = Error.new(response)
@@ -127,13 +153,22 @@ module OAuth2
127
153
 
128
154
  # Initializes an AccessToken by making a request to the token endpoint
129
155
  #
130
- # @param [Hash] params a Hash of params for the token endpoint
131
- # @param [Hash] access token options, to pass to the AccessToken object
132
- # @param [Class] class of access token for easier subclassing OAuth2::AccessToken
133
- # @return [AccessToken] the initalized AccessToken
134
- def get_token(params, access_token_opts = {}, access_token_class = AccessToken) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
135
- params = Authenticator.new(id, secret, options[:auth_scheme]).apply(params)
136
- opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
156
+ # @param params [Hash] a Hash of params for the token endpoint
157
+ # @param access_token_opts [Hash] access token options, to pass to the AccessToken object
158
+ # @param extract_access_token [Proc] proc that extracts the access token from the response (DEPRECATED)
159
+ # @param access_token_class [Class] class of access token for easier subclassing OAuth2::AccessToken, @version 2.0+
160
+ # @return [AccessToken] the initialized AccessToken
161
+ def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token], access_token_class: AccessToken)
162
+ params = params.map do |key, value|
163
+ if RESERVED_PARAM_KEYS.include?(key)
164
+ [key.to_sym, value]
165
+ else
166
+ [key, value]
167
+ end
168
+ end.to_h
169
+
170
+ params = authenticator.apply(params)
171
+ opts = {raise_errors: options[:raise_errors], parse: params.delete(:parse)}
137
172
  headers = params.delete(:headers) || {}
138
173
  if options[:token_method] == :post
139
174
  opts[:body] = params
@@ -143,38 +178,44 @@ module OAuth2
143
178
  opts[:headers] = {}
144
179
  end
145
180
  opts[:headers].merge!(headers)
146
- response = request(options[:token_method], token_url, opts)
147
- if options[:raise_errors] && !(response.parsed.is_a?(Hash) && response.parsed['access_token'])
148
- error = Error.new(response)
149
- raise(error)
181
+ http_method = options[:token_method]
182
+ http_method = :post if http_method == :post_with_query_string
183
+ response = request(http_method, token_url, opts)
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_with_legacy_extract(response, access_token_opts, extract_access_token)
190
+ else
191
+ parse_response(response, access_token_opts, access_token_class)
150
192
  end
151
- access_token_class.from_hash(self, response.parsed.merge(access_token_opts))
152
193
  end
153
194
 
154
195
  # The Authorization Code strategy
155
196
  #
156
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
197
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1
157
198
  def auth_code
158
199
  @auth_code ||= OAuth2::Strategy::AuthCode.new(self)
159
200
  end
160
201
 
161
202
  # The Implicit strategy
162
203
  #
163
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2
204
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2
164
205
  def implicit
165
206
  @implicit ||= OAuth2::Strategy::Implicit.new(self)
166
207
  end
167
208
 
168
209
  # The Resource Owner Password Credentials strategy
169
210
  #
170
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.3
211
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3
171
212
  def password
172
213
  @password ||= OAuth2::Strategy::Password.new(self)
173
214
  end
174
215
 
175
216
  # The Client Credentials strategy
176
217
  #
177
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.4
218
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4
178
219
  def client_credentials
179
220
  @client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self)
180
221
  end
@@ -194,10 +235,10 @@ module OAuth2
194
235
  #
195
236
  # @api semipublic
196
237
  #
197
- # @see https://tools.ietf.org/html/rfc6749#section-4.1
198
- # @see https://tools.ietf.org/html/rfc6749#section-4.1.3
199
- # @see https://tools.ietf.org/html/rfc6749#section-4.2.1
200
- # @see https://tools.ietf.org/html/rfc6749#section-10.6
238
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
239
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
240
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
241
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
201
242
  # @return [Hash] the params to add to a request or URL
202
243
  def redirection_params
203
244
  if options[:redirect_uri]
@@ -206,5 +247,62 @@ module OAuth2
206
247
  {}
207
248
  end
208
249
  end
250
+
251
+ private
252
+
253
+ # Returns the authenticator object
254
+ #
255
+ # @return [Authenticator] the initialized Authenticator
256
+ def authenticator
257
+ Authenticator.new(id, secret, options[:auth_scheme])
258
+ end
259
+
260
+ def parse_response_with_legacy_extract(response, access_token_opts, extract_access_token)
261
+ access_token = build_access_token_legacy_extract(response, access_token_opts, extract_access_token)
262
+
263
+ return access_token if access_token
264
+
265
+ if options[:raise_errors]
266
+ error = Error.new(response)
267
+ raise(error)
268
+ end
269
+
270
+ nil
271
+ end
272
+
273
+ def parse_response(response, access_token_opts, access_token_class)
274
+ data = response.parsed
275
+
276
+ unless data.is_a?(Hash) && access_token_class.contains_token?(data)
277
+ return unless options[:raise_errors]
278
+
279
+ error = Error.new(response)
280
+ raise(error)
281
+ end
282
+
283
+ build_access_token(response, access_token_opts, access_token_class)
284
+ end
285
+
286
+ # Builds the access token from the response of the HTTP call
287
+ #
288
+ # @return [AccessToken] the initialized AccessToken
289
+ def build_access_token(response, access_token_opts, access_token_class)
290
+ access_token_class.from_hash(self, response.parsed.merge(access_token_opts)).tap do |access_token|
291
+ access_token.response = response if access_token.respond_to?(:response=)
292
+ end
293
+ end
294
+
295
+ # Builds the access token from the response of the HTTP call with legacy extract_access_token
296
+ #
297
+ # @return [AccessToken] the initialized AccessToken
298
+ def build_access_token_legacy_extract(response, access_token_opts, extract_access_token)
299
+ extract_access_token.call(self, response.parsed.merge(access_token_opts))
300
+ rescue StandardError
301
+ nil
302
+ end
303
+
304
+ def oauth_debug_logging(builder)
305
+ builder.response :logger, options[:logger], bodies: true if ENV['OAUTH_DEBUG'] == 'true'
306
+ end
209
307
  end
210
308
  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