oauth2 1.4.9 → 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,33 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OAuth2
4
- class AccessToken
5
- attr_reader :client, :token, :expires_in, :expires_at, :params
6
- 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
7
7
 
8
- # Should these methods be deprecated?
9
8
  class << self
10
9
  # Initializes an AccessToken from a Hash
11
10
  #
12
- # @param [Client] the OAuth2::Client instance
13
- # @param [Hash] a hash of AccessToken property values
14
- # @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
15
14
  def from_hash(client, hash)
16
15
  hash = hash.dup
17
- 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)
18
17
  end
19
18
 
20
19
  # Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
21
20
  #
22
21
  # @param [Client] client the OAuth2::Client instance
23
22
  # @param [String] kvform the application/x-www-form-urlencoded string
24
- # @return [AccessToken] the initalized AccessToken
23
+ # @return [AccessToken] the initialized AccessToken
25
24
  def from_kvform(client, kvform)
26
25
  from_hash(client, Rack::Utils.parse_query(kvform))
27
26
  end
27
+
28
+ def contains_token?(hash)
29
+ hash.key?('access_token') || hash.key?('id_token') || hash.key?('token')
30
+ end
28
31
  end
29
32
 
30
- # Initalize an AccessToken
33
+ # Initialize an AccessToken
31
34
  #
32
35
  # @param [Client] client the OAuth2::Client instance
33
36
  # @param [String] token the Access Token value
@@ -35,6 +38,7 @@ module OAuth2
35
38
  # @option opts [String] :refresh_token (nil) the refresh_token value
36
39
  # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
37
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+
38
42
  # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
39
43
  # one of :header, :body or :query
40
44
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
@@ -44,16 +48,18 @@ module OAuth2
44
48
  @client = client
45
49
  @token = token.to_s
46
50
  opts = opts.dup
47
- [:refresh_token, :expires_in, :expires_at].each do |arg|
51
+ %i[refresh_token expires_in expires_at expires_latency].each do |arg|
48
52
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
49
53
  end
50
54
  @expires_in ||= opts.delete('expires')
51
55
  @expires_in &&= @expires_in.to_i
52
56
  @expires_at &&= convert_expires_at(@expires_at)
57
+ @expires_latency &&= @expires_latency.to_i
53
58
  @expires_at ||= Time.now.to_i + @expires_in if @expires_in
54
- @options = {:mode => opts.delete(:mode) || :header,
55
- :header_format => opts.delete(:header_format) || 'Bearer %s',
56
- :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'}
57
63
  @params = opts
58
64
  end
59
65
 
@@ -75,29 +81,32 @@ module OAuth2
75
81
  #
76
82
  # @return [Boolean]
77
83
  def expired?
78
- expires? && (expires_at < Time.now.to_i)
84
+ expires? && (expires_at <= Time.now.to_i)
79
85
  end
80
86
 
81
87
  # Refreshes the current Access Token
82
88
  #
83
89
  # @return [AccessToken] a new AccessToken
84
90
  # @note options should be carried over to the new AccessToken
85
- def refresh!(params = {})
91
+ def refresh(params = {}, access_token_opts = {}, access_token_class: self.class)
86
92
  raise('A refresh_token is not available') unless refresh_token
87
93
 
88
94
  params[:grant_type] = 'refresh_token'
89
95
  params[:refresh_token] = refresh_token
90
- new_token = @client.get_token(params)
96
+ new_token = @client.get_token(params, access_token_opts, access_token_class: access_token_class)
91
97
  new_token.options = options
92
98
  new_token.refresh_token = refresh_token unless new_token.refresh_token
93
99
  new_token
94
100
  end
101
+ # A compatibility alias
102
+ # @note does not modify the receiver, so bang is not the default method
103
+ alias refresh! refresh
95
104
 
96
105
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
97
106
  #
98
107
  # @return [Hash] a hash of AccessToken property values
99
108
  def to_hash
100
- 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)
101
110
  end
102
111
 
103
112
  # Make a request with the Access Token
@@ -166,7 +175,7 @@ module OAuth2
166
175
  if opts[:body].is_a?(Hash)
167
176
  opts[:body][options[:param_name]] = token
168
177
  else
169
- opts[:body] << "&#{options[:param_name]}=#{token}"
178
+ opts[:body] += "&#{options[:param_name]}=#{token}"
170
179
  end
171
180
  # @todo support for multi-part (file uploads)
172
181
  else
@@ -37,7 +37,7 @@ module OAuth2
37
37
  end
38
38
 
39
39
  def self.encode_basic_auth(user, password)
40
- 'Basic ' + Base64.encode64(user + ':' + password).delete("\n")
40
+ "Basic #{Base64.strict_encode64("#{user}:#{password}")}"
41
41
  end
42
42
 
43
43
  private
@@ -45,13 +45,18 @@ module OAuth2
45
45
  # Adds client_id and client_secret request parameters if they are not
46
46
  # already set.
47
47
  def apply_params_auth(params)
48
- {'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)
49
52
  end
50
53
 
51
54
  # When using schemes that don't require the client_secret to be passed i.e TLS Client Auth,
52
55
  # we don't want to send the secret
53
56
  def apply_client_id(params)
54
- {'client_id' => id}.merge(params)
57
+ result = {}
58
+ result['client_id'] = id unless id.nil?
59
+ result.merge(params)
55
60
  end
56
61
 
57
62
  # Adds an `Authorization` header with Basic Auth credentials if and only if
@@ -59,7 +64,7 @@ module OAuth2
59
64
  def apply_basic_auth(params)
60
65
  headers = params.fetch(:headers, {})
61
66
  headers = basic_auth_header.merge(headers)
62
- params.merge(:headers => headers)
67
+ params.merge(headers: headers)
63
68
  end
64
69
 
65
70
  # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
data/lib/oauth2/client.rb CHANGED
@@ -22,15 +22,15 @@ module OAuth2
22
22
  # @param [Hash] options the options to create the client with
23
23
  # @option options [String] :site the OAuth2 provider site host
24
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 or :post)
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
28
  # @option options [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
29
29
  # @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
30
30
  # @option options [FixNum] :max_redirects (5) maximum number of redirects to follow
31
31
  # @option options [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
32
32
  # @option options [Logger] :logger (::Logger.new($stdout)) which logger to use when OAUTH_DEBUG is enabled
33
- # @option options [Proc] (DEPRECATED) :extract_access_token proc that extracts the access token from the response
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)
34
34
  # @yield [builder] The Faraday connection builder
35
35
  def initialize(client_id, client_secret, options = {}, &block)
36
36
  opts = options.dup
@@ -38,16 +38,18 @@ module OAuth2
38
38
  @secret = client_secret
39
39
  @site = opts.delete(:site)
40
40
  ssl = opts.delete(:ssl)
41
- @options = {:authorize_url => 'oauth/authorize',
42
- :token_url => 'oauth/token',
43
- :token_method => :post,
44
- :auth_scheme => :request_body,
45
- :connection_opts => {},
46
- :connection_build => block,
47
- :max_redirects => 5,
48
- :raise_errors => true,
49
- :extract_access_token => DEFAULT_EXTRACT_ACCESS_TOKEN, # DEPRECATED
50
- :logger => ::Logger.new($stdout)}.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)
51
53
  @options[:connection_opts][:ssl] = ssl if ssl
52
54
  end
53
55
 
@@ -89,6 +91,9 @@ module OAuth2
89
91
  end
90
92
 
91
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
92
97
  #
93
98
  # @param [Symbol] verb one of :get, :post, :put, :delete
94
99
  # @param [String] url URL path of request
@@ -100,7 +105,7 @@ module OAuth2
100
105
  # code response for this request. Will default to client option
101
106
  # @option opts [Symbol] :parse @see Response::initialize
102
107
  # @yield [req] The Faraday request
103
- def request(verb, url, opts = {}) # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
108
+ def request(verb, url, opts = {})
104
109
  url = connection.build_url(url).to_s
105
110
 
106
111
  begin
@@ -112,7 +117,7 @@ module OAuth2
112
117
  raise ConnectionError, e
113
118
  end
114
119
 
115
- response = Response.new(response, :parse => opts[:parse])
120
+ response = Response.new(response, parse: opts[:parse])
116
121
 
117
122
  case response.status
118
123
  when 301, 302, 303, 307
@@ -126,7 +131,8 @@ module OAuth2
126
131
  end
127
132
  location = response.headers['location']
128
133
  if location
129
- request(verb, location, opts)
134
+ full_location = response.response.env.url.merge(location)
135
+ request(verb, full_location, opts)
130
136
  else
131
137
  error = Error.new(response)
132
138
  raise(error, "Got #{response.status} status code, but no Location header was present")
@@ -138,7 +144,6 @@ module OAuth2
138
144
  error = Error.new(response)
139
145
  raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
140
146
 
141
- response.error = error
142
147
  response
143
148
  else
144
149
  error = Error.new(response)
@@ -150,20 +155,20 @@ module OAuth2
150
155
  #
151
156
  # @param params [Hash] a Hash of params for the token endpoint
152
157
  # @param access_token_opts [Hash] access token options, to pass to the AccessToken object
153
- # @param access_token_class [Class] class of access token for easier subclassing OAuth2::AccessToken
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+
154
160
  # @return [AccessToken] the initialized AccessToken
155
- def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token]) # # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity Metrics/AbcSize, Metrics/MethodLength
161
+ def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token], access_token_class: AccessToken)
156
162
  params = params.map do |key, value|
157
163
  if RESERVED_PARAM_KEYS.include?(key)
158
164
  [key.to_sym, value]
159
165
  else
160
166
  [key, value]
161
167
  end
162
- end
163
- params = Hash[params]
168
+ end.to_h
164
169
 
165
170
  params = authenticator.apply(params)
166
- opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
171
+ opts = {raise_errors: options[:raise_errors], parse: params.delete(:parse)}
167
172
  headers = params.delete(:headers) || {}
168
173
  if options[:token_method] == :post
169
174
  opts[:body] = params
@@ -172,29 +177,19 @@ module OAuth2
172
177
  opts[:params] = params
173
178
  opts[:headers] = {}
174
179
  end
175
- opts[:headers] = opts[:headers].merge(headers)
180
+ opts[:headers].merge!(headers)
176
181
  http_method = options[:token_method]
182
+ http_method = :post if http_method == :post_with_query_string
177
183
  response = request(http_method, token_url, opts)
178
184
 
179
- access_token = begin
180
- build_access_token(response, access_token_opts, extract_access_token)
181
- rescue StandardError
182
- nil
183
- end
184
-
185
- response_contains_token = access_token || (
186
- response.parsed.is_a?(Hash) &&
187
- (response.parsed['access_token'] || response.parsed['id_token'])
188
- )
189
-
190
- if options[:raise_errors] && !response_contains_token
191
- error = Error.new(response)
192
- raise(error)
193
- elsif !response_contains_token
194
- return 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_with_legacy_extract(response, access_token_opts, extract_access_token)
190
+ else
191
+ parse_response(response, access_token_opts, access_token_class)
195
192
  end
196
-
197
- access_token
198
193
  end
199
194
 
200
195
  # The Authorization Code strategy
@@ -253,11 +248,6 @@ module OAuth2
253
248
  end
254
249
  end
255
250
 
256
- DEFAULT_EXTRACT_ACCESS_TOKEN = proc do |client, hash|
257
- token = hash.delete('access_token') || hash.delete(:access_token)
258
- token && AccessToken.new(client, token, hash)
259
- end
260
-
261
251
  private
262
252
 
263
253
  # Returns the authenticator object
@@ -267,26 +257,52 @@ module OAuth2
267
257
  Authenticator.new(id, secret, options[:auth_scheme])
268
258
  end
269
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
+
270
286
  # Builds the access token from the response of the HTTP call
271
287
  #
272
288
  # @return [AccessToken] the initialized AccessToken
273
- def build_access_token(response, access_token_opts, extract_access_token)
274
- parsed_response = response.parsed.dup
275
- return unless parsed_response.is_a?(Hash)
276
-
277
- hash = parsed_response.merge(access_token_opts)
278
-
279
- # Provide backwards compatibility for old AccessToken.form_hash pattern
280
- # Will be deprecated in 2.x
281
- if extract_access_token.is_a?(Class) && extract_access_token.respond_to?(:from_hash)
282
- extract_access_token.from_hash(self, hash)
283
- else
284
- extract_access_token.call(self, hash)
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=)
285
292
  end
286
293
  end
287
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
+
288
304
  def oauth_debug_logging(builder)
289
- builder.response :logger, options[:logger], :bodies => true if ENV['OAUTH_DEBUG'] == 'true'
305
+ builder.response :logger, options[:logger], bodies: true if ENV['OAUTH_DEBUG'] == 'true'
290
306
  end
291
307
  end
292
308
  end
data/lib/oauth2/error.rb CHANGED
@@ -4,39 +4,48 @@ module OAuth2
4
4
  class Error < StandardError
5
5
  attr_reader :response, :code, :description
6
6
 
7
- # standard error values include:
8
- # :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'
9
9
  def initialize(response)
10
- response.error = self
11
10
  @response = response
11
+ message_opts = {}
12
12
 
13
13
  if response.parsed.is_a?(Hash)
14
14
  @code = response.parsed['error']
15
15
  @description = response.parsed['error_description']
16
- error_description = "#{@code}: #{@description}"
16
+ message_opts = parse_error_description(@code, @description)
17
17
  end
18
18
 
19
- super(error_message(response.body, :error_description => error_description))
19
+ super(error_message(response.body, message_opts))
20
20
  end
21
21
 
22
- # Makes a error message
23
- # @param [String] response_body response body of request
24
- # @param [String] opts :error_description error description to show first line
22
+ private
23
+
25
24
  def error_message(response_body, opts = {})
26
- message = []
25
+ lines = []
26
+
27
+ lines << opts[:error_description] if opts[:error_description]
28
+
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
27
35
 
28
- opts[:error_description] && (message << opts[:error_description])
36
+ lines << error_string
37
+
38
+ lines.join("\n")
39
+ end
29
40
 
30
- error_message = if opts[:error_description] && opts[:error_description].respond_to?(:encoding)
31
- script_encoding = opts[:error_description].encoding
32
- response_body.encode(script_encoding, :invalid => :replace, :undef => :replace)
33
- else
34
- response_body
35
- end
41
+ def parse_error_description(code, description)
42
+ return {} unless code || description
36
43
 
37
- message << error_message
44
+ error_description = ''
45
+ error_description += "#{code}: " if code
46
+ error_description += description if description
38
47
 
39
- message.join("\n")
48
+ {error_description: error_description}
40
49
  end
41
50
  end
42
51
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'multi_json'
3
+ require 'json'
4
4
  require 'multi_xml'
5
5
  require 'rack'
6
6
 
@@ -8,20 +8,17 @@ module OAuth2
8
8
  # OAuth2::Response class
9
9
  class Response
10
10
  attr_reader :response
11
- attr_accessor :error, :options
11
+ attr_accessor :options
12
12
 
13
13
  # Procs that, when called, will parse a response body according
14
14
  # to the specified format.
15
15
  @@parsers = {
16
- :json => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable Style/RescueModifier
17
- :query => lambda { |body| Rack::Utils.parse_query(body) },
18
- :text => lambda { |body| body },
16
+ query: ->(body) { Rack::Utils.parse_query(body) },
17
+ text: ->(body) { body },
19
18
  }
20
19
 
21
20
  # Content type assignments for various potential HTTP content types.
22
21
  @@content_types = {
23
- 'application/json' => :json,
24
- 'text/javascript' => :json,
25
22
  'application/x-www-form-urlencoded' => :query,
26
23
  'text/plain' => :text,
27
24
  }
@@ -47,7 +44,7 @@ module OAuth2
47
44
  # :json, or :automatic (determined by Content-Type response header)
48
45
  def initialize(response, opts = {})
49
46
  @response = response
50
- @options = {:parse => :automatic}.merge(opts)
47
+ @options = {parse: :automatic}.merge(opts)
51
48
  end
52
49
 
53
50
  # The HTTP response headers
@@ -65,29 +62,74 @@ module OAuth2
65
62
  response.body || ''
66
63
  end
67
64
 
68
- # The parsed response body.
69
- # Will attempt to parse application/x-www-form-urlencoded and
70
- # application/json Content-Type response bodies
65
+ # The {#response} {#body} as parsed by {#parser}.
66
+ #
67
+ # @return [Object] As returned by {#parser} if it is #call-able.
68
+ # @return [nil] If the {#parser} is not #call-able.
71
69
  def parsed
72
- return nil unless @@parsers.key?(parser)
70
+ return @parsed if defined?(@parsed)
71
+
72
+ @parsed =
73
+ if parser.respond_to?(:call)
74
+ case parser.arity
75
+ when 0
76
+ parser.call
77
+ when 1
78
+ parser.call(body)
79
+ else
80
+ parser.call(body, response)
81
+ end
82
+ end
83
+
84
+ @parsed = OAuth2::SnakyHash.new(@parsed) if @parsed.is_a?(Hash)
73
85
 
74
- @parsed ||= @@parsers[parser].call(body)
86
+ @parsed
75
87
  end
76
88
 
77
89
  # Attempts to determine the content type of the response.
78
90
  def content_type
79
- ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
91
+ return nil unless response.headers
92
+
93
+ ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip.downcase
80
94
  end
81
95
 
82
- # Determines the parser that will be used to supply the content of #parsed
96
+ # Determines the parser (a Proc or other Object which responds to #call)
97
+ # that will be passed the {#body} (and optional {#response}) to supply
98
+ # {#parsed}.
99
+ #
100
+ # The parser can be supplied as the +:parse+ option in the form of a Proc
101
+ # (or other Object responding to #call) or a Symbol. In the latter case,
102
+ # the actual parser will be looked up in {@@parsers} by the supplied Symbol.
103
+ #
104
+ # If no +:parse+ option is supplied, the lookup Symbol will be determined
105
+ # by looking up {#content_type} in {@@content_types}.
106
+ #
107
+ # If {#parser} is a Proc, it will be called with no arguments, just
108
+ # {#body}, or {#body} and {#response}, depending on the Proc's arity.
109
+ #
110
+ # @return [Proc, #call] If a parser was found.
111
+ # @return [nil] If no parser was found.
83
112
  def parser
84
- return options[:parse].to_sym if @@parsers.key?(options[:parse])
113
+ return @parser if defined?(@parser)
114
+
115
+ @parser =
116
+ if options[:parse].respond_to?(:call)
117
+ options[:parse]
118
+ elsif options[:parse]
119
+ @@parsers[options[:parse].to_sym]
120
+ end
85
121
 
86
- @@content_types[content_type]
122
+ @parser ||= @@parsers[@@content_types[content_type]]
87
123
  end
88
124
  end
89
125
  end
90
126
 
91
- OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body|
92
- MultiXml.parse(body) rescue body # rubocop:disable Style/RescueModifier
127
+ OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml', 'application/xml']) do |body|
128
+ MultiXml.parse(body)
129
+ end
130
+
131
+ OAuth2::Response.register_parser(:json, ['application/json', 'text/javascript', 'application/hal+json', 'application/vnd.collection+json', 'application/vnd.api+json', 'application/problem+json']) do |body|
132
+ body = body.dup.force_encoding(::Encoding::ASCII_8BIT) if body.respond_to?(:force_encoding)
133
+
134
+ ::JSON.parse(body)
93
135
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OAuth2
4
+ # Hash which allow assign string key in camel case
5
+ # and query on both camel and snake case
6
+ class SnakyHash < ::Hashie::Mash::Rash
7
+ end
8
+ end