oauth2 1.4.3 → 2.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +188 -16
  3. data/CODE_OF_CONDUCT.md +105 -46
  4. data/CONTRIBUTING.md +27 -1
  5. data/LICENSE +1 -1
  6. data/README.md +428 -131
  7. data/SECURITY.md +26 -0
  8. data/lib/oauth2/access_token.rb +73 -25
  9. data/lib/oauth2/authenticator.rb +12 -5
  10. data/lib/oauth2/client.rb +208 -65
  11. data/lib/oauth2/error.rb +43 -24
  12. data/lib/oauth2/response.rb +81 -22
  13. data/lib/oauth2/strategy/assertion.rb +66 -39
  14. data/lib/oauth2/strategy/auth_code.rb +16 -3
  15. data/lib/oauth2/strategy/base.rb +2 -0
  16. data/lib/oauth2/strategy/client_credentials.rb +4 -2
  17. data/lib/oauth2/strategy/implicit.rb +10 -1
  18. data/lib/oauth2/strategy/password.rb +5 -3
  19. data/lib/oauth2/version.rb +3 -55
  20. data/lib/oauth2.rb +29 -1
  21. metadata +80 -100
  22. data/.document +0 -5
  23. data/.gitignore +0 -19
  24. data/.jrubyrc +0 -1
  25. data/.rspec +0 -2
  26. data/.rubocop.yml +0 -80
  27. data/.rubocop_rspec.yml +0 -26
  28. data/.rubocop_todo.yml +0 -15
  29. data/.ruby-version +0 -1
  30. data/.travis.yml +0 -87
  31. data/Gemfile +0 -40
  32. data/Rakefile +0 -45
  33. data/gemfiles/jruby_1.7.gemfile +0 -11
  34. data/gemfiles/jruby_9.0.gemfile +0 -7
  35. data/gemfiles/jruby_9.1.gemfile +0 -3
  36. data/gemfiles/jruby_9.2.gemfile +0 -3
  37. data/gemfiles/jruby_head.gemfile +0 -3
  38. data/gemfiles/ruby_1.9.gemfile +0 -11
  39. data/gemfiles/ruby_2.0.gemfile +0 -6
  40. data/gemfiles/ruby_2.1.gemfile +0 -6
  41. data/gemfiles/ruby_2.2.gemfile +0 -3
  42. data/gemfiles/ruby_2.3.gemfile +0 -3
  43. data/gemfiles/ruby_2.4.gemfile +0 -3
  44. data/gemfiles/ruby_2.5.gemfile +0 -3
  45. data/gemfiles/ruby_2.6.gemfile +0 -9
  46. data/gemfiles/ruby_2.7.gemfile +0 -9
  47. data/gemfiles/ruby_head.gemfile +0 -9
  48. data/gemfiles/truffleruby.gemfile +0 -3
  49. data/lib/oauth2/mac_token.rb +0 -122
  50. data/oauth2.gemspec +0 -52
data/SECURITY.md ADDED
@@ -0,0 +1,26 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported | EOL | Post-EOL / Enterprise |
6
+ |----------|-----------|---------|---------------------------------------|
7
+ | 2.latest | ✅ | 04/2024 | [Tidelift Subscription][tidelift-ref] |
8
+ | 1.latest | ✅ | 04/2023 | [Tidelift Subscription][tidelift-ref] |
9
+ | <= 1 | ⛔ | ⛔ | ⛔ |
10
+
11
+ ### EOL Policy
12
+
13
+ Non-commercial support for the oldest version of Ruby (which itself is going EOL) will be dropped each year in April.
14
+
15
+ ## Reporting a Vulnerability
16
+
17
+ To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
18
+ Tidelift will coordinate the fix and disclosure.
19
+
20
+ ## OAuth2 for Enterprise
21
+
22
+ Available as part of the Tidelift Subscription.
23
+
24
+ The maintainers of oauth2 and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. [Learn more.][tidelift-ref]
25
+
26
+ [tidelift-ref]: https://tidelift.com/subscription/pkg/rubygems-oauth2?utm_source=rubygems-oauth2&utm_medium=referral&utm_campaign=enterprise&utm_term=repo
@@ -1,56 +1,90 @@
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
+ TOKEN_KEYS_STR = %w[access_token id_token token accessToken idToken].freeze
6
+ TOKEN_KEYS_SYM = %i[access_token id_token token accessToken idToken].freeze
7
+ TOKEN_KEY_LOOKUP = TOKEN_KEYS_STR + TOKEN_KEYS_SYM
8
+
9
+ attr_reader :client, :token, :expires_in, :expires_at, :expires_latency, :params
10
+ attr_accessor :options, :refresh_token, :response
5
11
 
6
12
  class << self
7
13
  # Initializes an AccessToken from a Hash
8
14
  #
9
- # @param [Client] the OAuth2::Client instance
10
- # @param [Hash] a hash of AccessToken property values
11
- # @return [AccessToken] the initalized AccessToken
15
+ # @param [Client] client the OAuth2::Client instance
16
+ # @param [Hash] hash a hash of AccessToken property values
17
+ # @option hash [String] 'access_token', 'id_token', 'token', :access_token, :id_token, or :token the access token
18
+ # @return [AccessToken] the initialized AccessToken
12
19
  def from_hash(client, hash)
13
- hash = hash.dup
14
- new(client, hash.delete('access_token') || hash.delete(:access_token), hash)
20
+ fresh = hash.dup
21
+ supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
22
+ key = supported_keys[0]
23
+ extra_tokens_warning(supported_keys, key)
24
+ token = fresh.delete(key)
25
+ new(client, token, fresh)
15
26
  end
16
27
 
17
28
  # Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
18
29
  #
19
30
  # @param [Client] client the OAuth2::Client instance
20
31
  # @param [String] kvform the application/x-www-form-urlencoded string
21
- # @return [AccessToken] the initalized AccessToken
32
+ # @return [AccessToken] the initialized AccessToken
22
33
  def from_kvform(client, kvform)
23
34
  from_hash(client, Rack::Utils.parse_query(kvform))
24
35
  end
36
+
37
+ private
38
+
39
+ # Having too many is sus, and may lead to bugs. Having none is fine (e.g. refresh flow doesn't need a token).
40
+ def extra_tokens_warning(supported_keys, key)
41
+ return if OAuth2.config.silence_extra_tokens_warning
42
+ return if supported_keys.length <= 1
43
+
44
+ warn("OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (#{supported_keys}); using #{key.inspect}.")
45
+ end
25
46
  end
26
47
 
27
- # Initalize an AccessToken
48
+ # Initialize an AccessToken
28
49
  #
29
50
  # @param [Client] client the OAuth2::Client instance
30
- # @param [String] token the Access Token value
51
+ # @param [String] token the Access Token value (optional, may not be used in refresh flows)
31
52
  # @param [Hash] opts the options to create the Access Token with
32
53
  # @option opts [String] :refresh_token (nil) the refresh_token value
33
54
  # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
34
55
  # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
56
+ # @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
57
  # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
36
58
  # one of :header, :body or :query
37
59
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
38
60
  # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
39
61
  # Access Token value in :body or :query transmission mode
40
- def initialize(client, token, opts = {}) # rubocop:disable Metrics/AbcSize
62
+ def initialize(client, token, opts = {})
41
63
  @client = client
42
64
  @token = token.to_s
65
+
43
66
  opts = opts.dup
44
- [:refresh_token, :expires_in, :expires_at].each do |arg|
67
+ %i[refresh_token expires_in expires_at expires_latency].each do |arg|
45
68
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
46
69
  end
70
+ no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
71
+ if no_tokens
72
+ if @client.options[:raise_errors]
73
+ error = Error.new(opts)
74
+ raise(error)
75
+ else
76
+ warn('OAuth2::AccessToken has no token')
77
+ end
78
+ end
47
79
  @expires_in ||= opts.delete('expires')
48
80
  @expires_in &&= @expires_in.to_i
49
- @expires_at &&= @expires_at.to_i
81
+ @expires_at &&= convert_expires_at(@expires_at)
82
+ @expires_latency &&= @expires_latency.to_i
50
83
  @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'}
84
+ @expires_at -= @expires_latency if @expires_latency
85
+ @options = {mode: opts.delete(:mode) || :header,
86
+ header_format: opts.delete(:header_format) || 'Bearer %s',
87
+ param_name: opts.delete(:param_name) || 'access_token'}
54
88
  @params = opts
55
89
  end
56
90
 
@@ -72,28 +106,36 @@ module OAuth2
72
106
  #
73
107
  # @return [Boolean]
74
108
  def expired?
75
- expires? && (expires_at < Time.now.to_i)
109
+ expires? && (expires_at <= Time.now.to_i)
76
110
  end
77
111
 
78
112
  # Refreshes the current Access Token
79
113
  #
80
114
  # @return [AccessToken] a new AccessToken
81
115
  # @note options should be carried over to the new AccessToken
82
- def refresh!(params = {})
116
+ def refresh(params = {}, access_token_opts = {})
83
117
  raise('A refresh_token is not available') unless refresh_token
118
+
84
119
  params[:grant_type] = 'refresh_token'
85
120
  params[:refresh_token] = refresh_token
86
- new_token = @client.get_token(params)
121
+ new_token = @client.get_token(params, access_token_opts)
87
122
  new_token.options = options
88
- new_token.refresh_token = refresh_token unless new_token.refresh_token
123
+ if new_token.refresh_token
124
+ # Keep it, if there is one
125
+ else
126
+ new_token.refresh_token = refresh_token
127
+ end
89
128
  new_token
90
129
  end
130
+ # A compatibility alias
131
+ # @note does not modify the receiver, so bang is not the default method
132
+ alias refresh! refresh
91
133
 
92
134
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
93
135
  #
94
136
  # @return [Hash] a hash of AccessToken property values
95
137
  def to_hash
96
- params.merge(:access_token => token, :refresh_token => refresh_token, :expires_at => expires_at)
138
+ params.merge(access_token: token, refresh_token: refresh_token, expires_at: expires_at)
97
139
  end
98
140
 
99
141
  # Make a request with the Access Token
@@ -101,7 +143,7 @@ module OAuth2
101
143
  # @param [Symbol] verb the HTTP request method
102
144
  # @param [String] path the HTTP URL path of the request
103
145
  # @param [Hash] opts the options to make the request with
104
- # @see Client#request
146
+ # @see Client#request
105
147
  def request(verb, path, opts = {}, &block)
106
148
  configure_authentication!(opts)
107
149
  @client.request(verb, path, opts, &block)
@@ -149,7 +191,7 @@ module OAuth2
149
191
 
150
192
  private
151
193
 
152
- def configure_authentication!(opts) # rubocop:disable MethodLength, Metrics/AbcSize
194
+ def configure_authentication!(opts)
153
195
  case options[:mode]
154
196
  when :header
155
197
  opts[:headers] ||= {}
@@ -162,12 +204,18 @@ module OAuth2
162
204
  if opts[:body].is_a?(Hash)
163
205
  opts[:body][options[:param_name]] = token
164
206
  else
165
- opts[:body] << "&#{options[:param_name]}=#{token}"
207
+ opts[:body] += "&#{options[:param_name]}=#{token}"
166
208
  end
167
209
  # @todo support for multi-part (file uploads)
168
210
  else
169
211
  raise("invalid :mode option of #{options[:mode]}")
170
212
  end
171
213
  end
214
+
215
+ def convert_expires_at(expires_at)
216
+ Time.iso8601(expires_at.to_s).to_i
217
+ rescue ArgumentError
218
+ expires_at.to_i
219
+ end
172
220
  end
173
221
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
 
3
5
  module OAuth2
@@ -35,7 +37,7 @@ module OAuth2
35
37
  end
36
38
 
37
39
  def self.encode_basic_auth(user, password)
38
- 'Basic ' + Base64.encode64(user + ':' + password).delete("\n")
40
+ "Basic #{Base64.strict_encode64("#{user}:#{password}")}"
39
41
  end
40
42
 
41
43
  private
@@ -43,13 +45,18 @@ module OAuth2
43
45
  # Adds client_id and client_secret request parameters if they are not
44
46
  # already set.
45
47
  def apply_params_auth(params)
46
- {'client_id' => id, 'client_secret' => secret}.merge(params)
48
+ result = {}
49
+ result['client_id'] = id unless id.nil?
50
+ result['client_secret'] = secret unless secret.nil?
51
+ result.merge(params)
47
52
  end
48
53
 
49
54
  # When using schemes that don't require the client_secret to be passed i.e TLS Client Auth,
50
55
  # we don't want to send the secret
51
56
  def apply_client_id(params)
52
- { 'client_id' => id }.merge(params)
57
+ result = {}
58
+ result['client_id'] = id unless id.nil?
59
+ result.merge(params)
53
60
  end
54
61
 
55
62
  # Adds an `Authorization` header with Basic Auth credentials if and only if
@@ -57,10 +64,10 @@ module OAuth2
57
64
  def apply_basic_auth(params)
58
65
  headers = params.fetch(:headers, {})
59
66
  headers = basic_auth_header.merge(headers)
60
- params.merge(:headers => headers)
67
+ params.merge(headers: headers)
61
68
  end
62
69
 
63
- # @see https://tools.ietf.org/html/rfc2617#section-2
70
+ # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
64
71
  def basic_auth_header
65
72
  {'Authorization' => self.class.encode_basic_auth(id, secret)}
66
73
  end
data/lib/oauth2/client.rb CHANGED
@@ -1,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[body headers params parse snaky].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,35 +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).to_s
98
-
99
- response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
100
- req.params.update(opts[:params]) if opts[:params]
101
- yield(req) if block_given?
102
- end
103
- response = Response.new(response, :parse => opts[:parse])
111
+ # @option opts [true, false] :snaky (true) @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)
104
115
 
105
116
  case response.status
106
117
  when 301, 302, 303, 307
107
118
  opts[:redirect_count] ||= 0
108
119
  opts[:redirect_count] += 1
109
120
  return response if opts[:redirect_count] > options[:max_redirects]
121
+
110
122
  if response.status == 303
111
123
  verb = :get
112
124
  opts.delete(:body)
113
125
  end
114
- 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
115
134
  when 200..299, 300..399
116
135
  # on non-redirecting 3xx statuses, just return the response
117
136
  response
118
137
  when 400..599
119
138
  error = Error.new(response)
120
139
  raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
121
- response.error = error
140
+
122
141
  response
123
142
  else
124
143
  error = Error.new(response)
@@ -128,54 +147,84 @@ module OAuth2
128
147
 
129
148
  # Initializes an AccessToken by making a request to the token endpoint
130
149
  #
131
- # @param [Hash] params a Hash of params for the token endpoint
132
- # @param [Hash] access token options, to pass to the AccessToken object
133
- # @param [Class] class of access token for easier subclassing OAuth2::AccessToken
150
+ # @param params [Hash] a Hash of params for the token endpoint, except:
151
+ # @option params [Symbol] :parse @see Response#initialize
152
+ # @option params [true, false] :snaky (true) @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
134
156
  # @return [AccessToken] the initialized AccessToken
135
- def get_token(params, access_token_opts = {}, access_token_class = AccessToken) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
136
- params = Authenticator.new(id, secret, options[:auth_scheme]).apply(params)
137
- opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
138
- headers = params.delete(:headers) || {}
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
+ parse, snaky, params, headers = parse_snaky_params_headers(params)
161
+
162
+ request_opts = {
163
+ raise_errors: options[:raise_errors],
164
+ parse: parse,
165
+ snaky: snaky,
166
+ }
139
167
  if options[:token_method] == :post
140
- opts[:body] = params
141
- opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
168
+
169
+ # NOTE: If proliferation of request types continues we should implement a parser solution for Request,
170
+ # just like we have with Response.
171
+ request_opts[:body] = if headers['Content-Type'] == 'application/json'
172
+ params.to_json
173
+ else
174
+ params
175
+ end
176
+
177
+ request_opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
142
178
  else
143
- opts[:params] = params
144
- opts[:headers] = {}
179
+ request_opts[:params] = params
180
+ request_opts[:headers] = {}
145
181
  end
146
- opts[:headers].merge!(headers)
147
- response = request(options[:token_method], token_url, opts)
148
- if options[:raise_errors] && !(response.parsed.is_a?(Hash) && response.parsed['access_token'])
149
- error = Error.new(response)
150
- raise(error)
182
+ request_opts[:headers].merge!(headers)
183
+ response = request(http_method, token_url, request_opts, &block)
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_legacy(response, access_token_opts, extract_access_token)
190
+ else
191
+ parse_response(response, access_token_opts)
151
192
  end
152
- access_token_class.from_hash(self, response.parsed.merge(access_token_opts))
193
+ end
194
+
195
+ # The HTTP Method of the request
196
+ # @return [Symbol] HTTP verb, one of :get, :post, :put, :delete
197
+ def http_method
198
+ http_meth = options[:token_method].to_sym
199
+ return :post if http_meth == :post_with_query_string
200
+
201
+ http_meth
153
202
  end
154
203
 
155
204
  # The Authorization Code strategy
156
205
  #
157
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
206
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1
158
207
  def auth_code
159
208
  @auth_code ||= OAuth2::Strategy::AuthCode.new(self)
160
209
  end
161
210
 
162
211
  # The Implicit strategy
163
212
  #
164
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2
213
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2
165
214
  def implicit
166
215
  @implicit ||= OAuth2::Strategy::Implicit.new(self)
167
216
  end
168
217
 
169
218
  # The Resource Owner Password Credentials strategy
170
219
  #
171
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.3
220
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3
172
221
  def password
173
222
  @password ||= OAuth2::Strategy::Password.new(self)
174
223
  end
175
224
 
176
225
  # The Client Credentials strategy
177
226
  #
178
- # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.4
227
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4
179
228
  def client_credentials
180
229
  @client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self)
181
230
  end
@@ -195,10 +244,10 @@ module OAuth2
195
244
  #
196
245
  # @api semipublic
197
246
  #
198
- # @see https://tools.ietf.org/html/rfc6749#section-4.1
199
- # @see https://tools.ietf.org/html/rfc6749#section-4.1.3
200
- # @see https://tools.ietf.org/html/rfc6749#section-4.2.1
201
- # @see https://tools.ietf.org/html/rfc6749#section-10.6
247
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
248
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
249
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
250
+ # @see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
202
251
  # @return [Hash] the params to add to a request or URL
203
252
  def redirection_params
204
253
  if options[:redirect_uri]
@@ -207,5 +256,99 @@ module OAuth2
207
256
  {}
208
257
  end
209
258
  end
259
+
260
+ private
261
+
262
+ def parse_snaky_params_headers(params)
263
+ params = params.map do |key, value|
264
+ if RESERVED_PARAM_KEYS.include?(key)
265
+ [key.to_sym, value]
266
+ else
267
+ [key, value]
268
+ end
269
+ end.to_h
270
+ parse = params.key?(:parse) ? params.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
271
+ snaky = params.key?(:snaky) ? params.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
272
+ params = authenticator.apply(params)
273
+ # authenticator may add :headers, and we remove them here
274
+ headers = params.delete(:headers) || {}
275
+ [parse, snaky, params, headers]
276
+ end
277
+
278
+ def execute_request(verb, url, opts = {})
279
+ url = connection.build_url(url).to_s
280
+
281
+ begin
282
+ response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
283
+ req.params.update(opts[:params]) if opts[:params]
284
+ yield(req) if block_given?
285
+ end
286
+ rescue Faraday::ConnectionFailed => e
287
+ raise ConnectionError, e
288
+ rescue Faraday::TimeoutError => e
289
+ raise TimeoutError, e
290
+ end
291
+
292
+ parse = opts.key?(:parse) ? opts.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
293
+ snaky = opts.key?(:snaky) ? opts.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
294
+
295
+ Response.new(response, parse: parse, snaky: snaky)
296
+ end
297
+
298
+ # Returns the authenticator object
299
+ #
300
+ # @return [Authenticator] the initialized Authenticator
301
+ def authenticator
302
+ Authenticator.new(id, secret, options[:auth_scheme])
303
+ end
304
+
305
+ def parse_response_legacy(response, access_token_opts, extract_access_token)
306
+ access_token = build_access_token_legacy(response, access_token_opts, extract_access_token)
307
+
308
+ return access_token if access_token
309
+
310
+ if options[:raise_errors]
311
+ error = Error.new(response)
312
+ raise(error)
313
+ end
314
+
315
+ nil
316
+ end
317
+
318
+ def parse_response(response, access_token_opts)
319
+ access_token_class = options[:access_token_class]
320
+ data = response.parsed
321
+
322
+ unless data.is_a?(Hash) && !data.empty?
323
+ return unless options[:raise_errors]
324
+
325
+ error = Error.new(response)
326
+ raise(error)
327
+ end
328
+
329
+ build_access_token(response, access_token_opts, access_token_class)
330
+ end
331
+
332
+ # Builds the access token from the response of the HTTP call
333
+ #
334
+ # @return [AccessToken] the initialized AccessToken
335
+ def build_access_token(response, access_token_opts, access_token_class)
336
+ access_token_class.from_hash(self, response.parsed.merge(access_token_opts)).tap do |access_token|
337
+ access_token.response = response if access_token.respond_to?(:response=)
338
+ end
339
+ end
340
+
341
+ # Builds the access token from the response of the HTTP call with legacy extract_access_token
342
+ #
343
+ # @return [AccessToken] the initialized AccessToken
344
+ def build_access_token_legacy(response, access_token_opts, extract_access_token)
345
+ extract_access_token.call(self, response.parsed.merge(access_token_opts))
346
+ rescue StandardError
347
+ nil
348
+ end
349
+
350
+ def oauth_debug_logging(builder)
351
+ builder.response :logger, options[:logger], bodies: true if ENV['OAUTH_DEBUG'] == 'true'
352
+ end
210
353
  end
211
354
  end