oauth2 1.4.0 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 = {})
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)
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
@@ -101,7 +114,7 @@ module OAuth2
101
114
  # @param [Symbol] verb the HTTP request method
102
115
  # @param [String] path the HTTP URL path of the request
103
116
  # @param [Hash] opts the options to make the request with
104
- # @see Client#request
117
+ # @see Client#request
105
118
  def request(verb, path, opts = {}, &block)
106
119
  configure_authentication!(opts)
107
120
  @client.request(verb, path, opts, &block)
@@ -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,16 @@
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
12
+ RESERVED_PARAM_KEYS = %w[headers parse].freeze
13
+
7
14
  attr_reader :id, :secret, :site
8
15
  attr_accessor :options
9
16
  attr_writer :connection
@@ -14,17 +21,19 @@ module OAuth2
14
21
  #
15
22
  # @param [String] client_id the client_id value
16
23
  # @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
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+
28
37
  # @yield [builder] The Faraday connection builder
29
38
  def initialize(client_id, client_secret, options = {}, &block)
30
39
  opts = options.dup
@@ -32,20 +41,25 @@ module OAuth2
32
41
  @secret = client_secret
33
42
  @site = opts.delete(:site)
34
43
  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)
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]
45
+ @options = {
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,
56
+ }.merge(opts)
43
57
  @options[:connection_opts][:ssl] = ssl if ssl
44
58
  end
45
59
 
46
60
  # Set the site host
47
61
  #
48
- # @param [String] the OAuth2 provider site host
62
+ # @param value [String] the OAuth2 provider site host
49
63
  def site=(value)
50
64
  @connection = nil
51
65
  @site = value
@@ -53,15 +67,16 @@ module OAuth2
53
67
 
54
68
  # The Faraday connection object
55
69
  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)
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
61
78
  end
62
79
  end
63
- conn
64
- end
65
80
  end
66
81
 
67
82
  # The authorize endpoint URL of the OAuth2 provider
@@ -80,6 +95,9 @@ module OAuth2
80
95
  end
81
96
 
82
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
83
101
  #
84
102
  # @param [Symbol] verb one of :get, :post, :put, :delete
85
103
  # @param [String] url URL path of request
@@ -90,34 +108,36 @@ module OAuth2
90
108
  # @option opts [Boolean] :raise_errors whether or not to raise an OAuth2::Error on 400+ status
91
109
  # code response for this request. Will default to client option
92
110
  # @option opts [Symbol] :parse @see Response::initialize
93
- # @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
98
-
99
- response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
100
- yield(req) if block_given?
101
- end
102
- 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)
103
115
 
104
116
  case response.status
105
117
  when 301, 302, 303, 307
106
118
  opts[:redirect_count] ||= 0
107
119
  opts[:redirect_count] += 1
108
120
  return response if opts[:redirect_count] > options[:max_redirects]
121
+
109
122
  if response.status == 303
110
123
  verb = :get
111
124
  opts.delete(:body)
112
125
  end
113
- 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
114
134
  when 200..299, 300..399
115
135
  # on non-redirecting 3xx statuses, just return the response
116
136
  response
117
137
  when 400..599
118
138
  error = Error.new(response)
119
139
  raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
120
- response.error = error
140
+
121
141
  response
122
142
  else
123
143
  error = Error.new(response)
@@ -127,54 +147,78 @@ module OAuth2
127
147
 
128
148
  # Initializes an AccessToken by making a request to the token endpoint
129
149
  #
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)}
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
156
+ # @return [AccessToken] the initialized AccessToken
157
+ def get_token(params, access_token_opts = {}, extract_access_token = nil, &block)
158
+ warn('OAuth2::Client#get_token argument `extract_access_token` will be removed in oauth2 v3. Refactor to use `access_token_class` on #initialize.') if extract_access_token
159
+ extract_access_token ||= options[:extract_access_token]
160
+ params = params.map do |key, value|
161
+ if RESERVED_PARAM_KEYS.include?(key)
162
+ [key.to_sym, value]
163
+ else
164
+ [key, value]
165
+ end
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
+ }
173
+
174
+ params = authenticator.apply(params)
137
175
  headers = params.delete(:headers) || {}
138
176
  if options[:token_method] == :post
139
- opts[:body] = params
140
- 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'}
141
179
  else
142
- opts[:params] = params
143
- opts[:headers] = {}
180
+ request_opts[:params] = params
181
+ request_opts[:headers] = {}
144
182
  end
145
- 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)
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)
187
+
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)
150
195
  end
151
- access_token_class.from_hash(self, response.parsed.merge(access_token_opts))
152
196
  end
153
197
 
154
198
  # The Authorization Code strategy
155
199
  #
156
- # @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
157
201
  def auth_code
158
202
  @auth_code ||= OAuth2::Strategy::AuthCode.new(self)
159
203
  end
160
204
 
161
205
  # The Implicit strategy
162
206
  #
163
- # @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
164
208
  def implicit
165
209
  @implicit ||= OAuth2::Strategy::Implicit.new(self)
166
210
  end
167
211
 
168
212
  # The Resource Owner Password Credentials strategy
169
213
  #
170
- # @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
171
215
  def password
172
216
  @password ||= OAuth2::Strategy::Password.new(self)
173
217
  end
174
218
 
175
219
  # The Client Credentials strategy
176
220
  #
177
- # @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
178
222
  def client_credentials
179
223
  @client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self)
180
224
  end
@@ -194,10 +238,10 @@ module OAuth2
194
238
  #
195
239
  # @api semipublic
196
240
  #
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
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
201
245
  # @return [Hash] the params to add to a request or URL
202
246
  def redirection_params
203
247
  if options[:redirect_uri]
@@ -206,5 +250,80 @@ module OAuth2
206
250
  {}
207
251
  end
208
252
  end
253
+
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])
271
+ end
272
+
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
279
+
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)
282
+
283
+ return access_token if access_token
284
+
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)
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'
327
+ end
209
328
  end
210
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