oauth2 1.4.10 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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/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