oauth2 1.4.7 → 1.4.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +90 -72
- data/CONTRIBUTING.md +44 -0
- data/LICENSE +1 -1
- data/README.md +331 -128
- data/SECURITY.md +26 -0
- data/lib/oauth2/access_token.rb +4 -2
- data/lib/oauth2/authenticator.rb +3 -1
- data/lib/oauth2/client.rb +91 -56
- data/lib/oauth2/error.rb +3 -1
- data/lib/oauth2/mac_token.rb +19 -19
- data/lib/oauth2/response.rb +2 -0
- data/lib/oauth2/strategy/assertion.rb +3 -1
- data/lib/oauth2/strategy/auth_code.rb +3 -1
- data/lib/oauth2/strategy/base.rb +2 -0
- data/lib/oauth2/strategy/client_credentials.rb +3 -1
- data/lib/oauth2/strategy/implicit.rb +3 -1
- data/lib/oauth2/strategy/password.rb +3 -1
- data/lib/oauth2/version.rb +1 -1
- data/lib/oauth2.rb +2 -0
- metadata +68 -98
- data/spec/helper.rb +0 -37
- data/spec/oauth2/access_token_spec.rb +0 -216
- data/spec/oauth2/authenticator_spec.rb +0 -84
- data/spec/oauth2/client_spec.rb +0 -506
- data/spec/oauth2/mac_token_spec.rb +0 -117
- data/spec/oauth2/response_spec.rb +0 -90
- data/spec/oauth2/strategy/assertion_spec.rb +0 -58
- data/spec/oauth2/strategy/auth_code_spec.rb +0 -107
- data/spec/oauth2/strategy/base_spec.rb +0 -5
- data/spec/oauth2/strategy/client_credentials_spec.rb +0 -69
- data/spec/oauth2/strategy/implicit_spec.rb +0 -26
- data/spec/oauth2/strategy/password_spec.rb +0 -55
- data/spec/oauth2/version_spec.rb +0 -23
data/lib/oauth2/client.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'faraday'
|
2
4
|
require 'logger'
|
3
5
|
|
4
6
|
module OAuth2
|
7
|
+
ConnectionError = Class.new(Faraday::ConnectionFailed)
|
5
8
|
# The OAuth2::Client class
|
6
9
|
class Client # rubocop:disable Metrics/ClassLength
|
7
10
|
RESERVED_PARAM_KEYS = %w[headers parse].freeze
|
@@ -16,17 +19,18 @@ module OAuth2
|
|
16
19
|
#
|
17
20
|
# @param [String] client_id the client_id value
|
18
21
|
# @param [String] client_secret the client_secret value
|
19
|
-
# @param [Hash]
|
20
|
-
# @option
|
21
|
-
# @option
|
22
|
-
# @option
|
23
|
-
# @option
|
24
|
-
# @option
|
25
|
-
# @option
|
26
|
-
# @option
|
27
|
-
# @option
|
28
|
-
# @option
|
29
|
-
# @option
|
22
|
+
# @param [Hash] options the options to create the client with
|
23
|
+
# @option options [String] :site the OAuth2 provider site host
|
24
|
+
# @option options [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange
|
25
|
+
# @option options [String] :authorize_url ('oauth/authorize') absolute or relative URL path to the Authorization endpoint
|
26
|
+
# @option options [String] :token_url ('oauth/token') absolute or relative URL path to the Token endpoint
|
27
|
+
# @option options [Symbol] :token_method (:post) HTTP method to use to request token (:get or :post)
|
28
|
+
# @option options [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
|
29
|
+
# @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
|
30
|
+
# @option options [FixNum] :max_redirects (5) maximum number of redirects to follow
|
31
|
+
# @option options [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
|
32
|
+
# @option options [Logger] :logger (::Logger.new($stdout)) which logger to use when OAUTH_DEBUG is enabled
|
33
|
+
# @option options [Proc] (DEPRECATED) :extract_access_token proc that extracts the access token from the response
|
30
34
|
# @yield [builder] The Faraday connection builder
|
31
35
|
def initialize(client_id, client_secret, options = {}, &block)
|
32
36
|
opts = options.dup
|
@@ -34,24 +38,22 @@ module OAuth2
|
|
34
38
|
@secret = client_secret
|
35
39
|
@site = opts.delete(:site)
|
36
40
|
ssl = opts.delete(:ssl)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
:extract_access_token => DEFAULT_EXTRACT_ACCESS_TOKEN,
|
48
|
-
}.merge(opts)
|
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)
|
49
51
|
@options[:connection_opts][:ssl] = ssl if ssl
|
50
52
|
end
|
51
53
|
|
52
54
|
# Set the site host
|
53
55
|
#
|
54
|
-
# @param [String] the OAuth2 provider site host
|
56
|
+
# @param value [String] the OAuth2 provider site host
|
55
57
|
def site=(value)
|
56
58
|
@connection = nil
|
57
59
|
@site = value
|
@@ -59,15 +61,16 @@ module OAuth2
|
|
59
61
|
|
60
62
|
# The Faraday connection object
|
61
63
|
def connection
|
62
|
-
@connection ||=
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
options[:connection_build].call(
|
64
|
+
@connection ||=
|
65
|
+
Faraday.new(site, options[:connection_opts]) do |builder|
|
66
|
+
oauth_debug_logging(builder)
|
67
|
+
if options[:connection_build]
|
68
|
+
options[:connection_build].call(builder)
|
69
|
+
else
|
70
|
+
builder.request :url_encoded # form-encode POST params
|
71
|
+
builder.adapter Faraday.default_adapter # make requests with Net::HTTP
|
67
72
|
end
|
68
73
|
end
|
69
|
-
conn
|
70
|
-
end
|
71
74
|
end
|
72
75
|
|
73
76
|
# The authorize endpoint URL of the OAuth2 provider
|
@@ -97,15 +100,18 @@ module OAuth2
|
|
97
100
|
# code response for this request. Will default to client option
|
98
101
|
# @option opts [Symbol] :parse @see Response::initialize
|
99
102
|
# @yield [req] The Faraday request
|
100
|
-
def request(verb, url, opts = {}) # rubocop:disable Metrics/
|
101
|
-
connection.response :logger, ::Logger.new($stdout) if ENV['OAUTH_DEBUG'] == 'true'
|
102
|
-
|
103
|
+
def request(verb, url, opts = {}) # rubocop:disable Metrics/AbcSize
|
103
104
|
url = connection.build_url(url).to_s
|
104
105
|
|
105
|
-
|
106
|
-
|
107
|
-
|
106
|
+
begin
|
107
|
+
response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
|
108
|
+
req.params.update(opts[:params]) if opts[:params]
|
109
|
+
yield(req) if block_given?
|
110
|
+
end
|
111
|
+
rescue Faraday::ConnectionFailed => e
|
112
|
+
raise ConnectionError, e
|
108
113
|
end
|
114
|
+
|
109
115
|
response = Response.new(response, :parse => opts[:parse])
|
110
116
|
|
111
117
|
case response.status
|
@@ -118,7 +124,13 @@ module OAuth2
|
|
118
124
|
verb = :get
|
119
125
|
opts.delete(:body)
|
120
126
|
end
|
121
|
-
|
127
|
+
location = response.headers['location']
|
128
|
+
if location
|
129
|
+
request(verb, 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
|
122
134
|
when 200..299, 300..399
|
123
135
|
# on non-redirecting 3xx statuses, just return the response
|
124
136
|
response
|
@@ -136,11 +148,11 @@ module OAuth2
|
|
136
148
|
|
137
149
|
# Initializes an AccessToken by making a request to the token endpoint
|
138
150
|
#
|
139
|
-
# @param [Hash]
|
140
|
-
# @param [Hash] access token options, to pass to the AccessToken object
|
141
|
-
# @param [Class] class of access token for easier subclassing OAuth2::AccessToken
|
151
|
+
# @param params [Hash] a Hash of params for the token endpoint
|
152
|
+
# @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
|
142
154
|
# @return [AccessToken] the initialized AccessToken
|
143
|
-
def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token]) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
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
|
144
156
|
params = params.map do |key, value|
|
145
157
|
if RESERVED_PARAM_KEYS.include?(key)
|
146
158
|
[key.to_sym, value]
|
@@ -150,7 +162,7 @@ module OAuth2
|
|
150
162
|
end
|
151
163
|
params = Hash[params]
|
152
164
|
|
153
|
-
params =
|
165
|
+
params = authenticator.apply(params)
|
154
166
|
opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
|
155
167
|
headers = params.delete(:headers) || {}
|
156
168
|
if options[:token_method] == :post
|
@@ -160,8 +172,9 @@ module OAuth2
|
|
160
172
|
opts[:params] = params
|
161
173
|
opts[:headers] = {}
|
162
174
|
end
|
163
|
-
opts[:headers].merge
|
164
|
-
|
175
|
+
opts[:headers] = opts[:headers].merge(headers)
|
176
|
+
http_method = options[:token_method]
|
177
|
+
response = request(http_method, token_url, opts)
|
165
178
|
|
166
179
|
access_token = begin
|
167
180
|
build_access_token(response, access_token_opts, extract_access_token)
|
@@ -169,37 +182,45 @@ module OAuth2
|
|
169
182
|
nil
|
170
183
|
end
|
171
184
|
|
172
|
-
|
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
|
173
191
|
error = Error.new(response)
|
174
192
|
raise(error)
|
193
|
+
elsif !response_contains_token
|
194
|
+
return nil
|
175
195
|
end
|
196
|
+
|
176
197
|
access_token
|
177
198
|
end
|
178
199
|
|
179
200
|
# The Authorization Code strategy
|
180
201
|
#
|
181
|
-
# @see http://
|
202
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1
|
182
203
|
def auth_code
|
183
204
|
@auth_code ||= OAuth2::Strategy::AuthCode.new(self)
|
184
205
|
end
|
185
206
|
|
186
207
|
# The Implicit strategy
|
187
208
|
#
|
188
|
-
# @see http://
|
209
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2
|
189
210
|
def implicit
|
190
211
|
@implicit ||= OAuth2::Strategy::Implicit.new(self)
|
191
212
|
end
|
192
213
|
|
193
214
|
# The Resource Owner Password Credentials strategy
|
194
215
|
#
|
195
|
-
# @see http://
|
216
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3
|
196
217
|
def password
|
197
218
|
@password ||= OAuth2::Strategy::Password.new(self)
|
198
219
|
end
|
199
220
|
|
200
221
|
# The Client Credentials strategy
|
201
222
|
#
|
202
|
-
# @see http://
|
223
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4
|
203
224
|
def client_credentials
|
204
225
|
@client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self)
|
205
226
|
end
|
@@ -219,10 +240,10 @@ module OAuth2
|
|
219
240
|
#
|
220
241
|
# @api semipublic
|
221
242
|
#
|
222
|
-
# @see https://
|
223
|
-
# @see https://
|
224
|
-
# @see https://
|
225
|
-
# @see https://
|
243
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
|
244
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
|
245
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
|
246
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
|
226
247
|
# @return [Hash] the params to add to a request or URL
|
227
248
|
def redirection_params
|
228
249
|
if options[:redirect_uri]
|
@@ -239,19 +260,33 @@ module OAuth2
|
|
239
260
|
|
240
261
|
private
|
241
262
|
|
263
|
+
# Returns the authenticator object
|
264
|
+
#
|
265
|
+
# @return [Authenticator] the initialized Authenticator
|
266
|
+
def authenticator
|
267
|
+
Authenticator.new(id, secret, options[:auth_scheme])
|
268
|
+
end
|
269
|
+
|
270
|
+
# Builds the access token from the response of the HTTP call
|
271
|
+
#
|
272
|
+
# @return [AccessToken] the initialized AccessToken
|
242
273
|
def build_access_token(response, access_token_opts, extract_access_token)
|
243
274
|
parsed_response = response.parsed.dup
|
244
275
|
return unless parsed_response.is_a?(Hash)
|
245
276
|
|
246
277
|
hash = parsed_response.merge(access_token_opts)
|
247
278
|
|
248
|
-
# Provide backwards compatibility for old
|
249
|
-
#
|
279
|
+
# Provide backwards compatibility for old AccessToken.form_hash pattern
|
280
|
+
# Will be deprecated in 2.x
|
250
281
|
if extract_access_token.is_a?(Class) && extract_access_token.respond_to?(:from_hash)
|
251
282
|
extract_access_token.from_hash(self, hash)
|
252
283
|
else
|
253
284
|
extract_access_token.call(self, hash)
|
254
285
|
end
|
255
286
|
end
|
287
|
+
|
288
|
+
def oauth_debug_logging(builder)
|
289
|
+
builder.response :logger, options[:logger], :bodies => true if ENV['OAUTH_DEBUG'] == 'true'
|
290
|
+
end
|
256
291
|
end
|
257
292
|
end
|
data/lib/oauth2/error.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module OAuth2
|
2
4
|
class Error < StandardError
|
3
5
|
attr_reader :response, :code, :description
|
@@ -23,7 +25,7 @@ module OAuth2
|
|
23
25
|
def error_message(response_body, opts = {})
|
24
26
|
message = []
|
25
27
|
|
26
|
-
opts[:error_description] && message << opts[:error_description]
|
28
|
+
opts[:error_description] && (message << opts[:error_description])
|
27
29
|
|
28
30
|
error_message = if opts[:error_description] && opts[:error_description].respond_to?(:encoding)
|
29
31
|
script_encoding = opts[:error_description].encoding
|
data/lib/oauth2/mac_token.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'base64'
|
2
4
|
require 'digest'
|
3
5
|
require 'openssl'
|
@@ -60,7 +62,7 @@ module OAuth2
|
|
60
62
|
# @param [String] url the HTTP URL path of the request
|
61
63
|
def header(verb, url)
|
62
64
|
timestamp = Time.now.utc.to_i
|
63
|
-
nonce = Digest::
|
65
|
+
nonce = Digest::SHA256.hexdigest([timestamp, SecureRandom.hex].join(':'))
|
64
66
|
|
65
67
|
uri = URI.parse(url)
|
66
68
|
|
@@ -95,24 +97,22 @@ module OAuth2
|
|
95
97
|
#
|
96
98
|
# @param [String] alg the algorithm to use (one of 'hmac-sha-1', 'hmac-sha-256')
|
97
99
|
def algorithm=(alg)
|
98
|
-
@algorithm =
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
end
|
115
|
-
end
|
100
|
+
@algorithm = case alg.to_s
|
101
|
+
when 'hmac-sha-1'
|
102
|
+
begin
|
103
|
+
OpenSSL::Digest('SHA1').new
|
104
|
+
rescue StandardError
|
105
|
+
OpenSSL::Digest.new('SHA1')
|
106
|
+
end
|
107
|
+
when 'hmac-sha-256'
|
108
|
+
begin
|
109
|
+
OpenSSL::Digest('SHA256').new
|
110
|
+
rescue StandardError
|
111
|
+
OpenSSL::Digest.new('SHA256')
|
112
|
+
end
|
113
|
+
else
|
114
|
+
raise(ArgumentError, 'Unsupported algorithm')
|
115
|
+
end
|
116
116
|
end
|
117
117
|
|
118
118
|
private
|
data/lib/oauth2/response.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'jwt'
|
2
4
|
|
3
5
|
module OAuth2
|
4
6
|
module Strategy
|
5
7
|
# The Client Assertion Strategy
|
6
8
|
#
|
7
|
-
# @see
|
9
|
+
# @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-10#section-4.1.3
|
8
10
|
#
|
9
11
|
# Sample usage:
|
10
12
|
# client = OAuth2::Client.new(client_id, client_secret,
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module OAuth2
|
2
4
|
module Strategy
|
3
5
|
# The Authorization Code Strategy
|
4
6
|
#
|
5
|
-
# @see http://
|
7
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1
|
6
8
|
class AuthCode < Base
|
7
9
|
# The required query parameters for the authorize URL
|
8
10
|
#
|
data/lib/oauth2/strategy/base.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module OAuth2
|
2
4
|
module Strategy
|
3
5
|
# The Client Credentials Strategy
|
4
6
|
#
|
5
|
-
# @see http://
|
7
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4
|
6
8
|
class ClientCredentials < Base
|
7
9
|
# Not used for this strategy
|
8
10
|
#
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module OAuth2
|
2
4
|
module Strategy
|
3
5
|
# The Implicit Strategy
|
4
6
|
#
|
5
|
-
# @see http://
|
7
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2
|
6
8
|
class Implicit < Base
|
7
9
|
# The required query parameters for the authorize URL
|
8
10
|
#
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module OAuth2
|
2
4
|
module Strategy
|
3
5
|
# The Resource Owner Password Credentials Authorization Strategy
|
4
6
|
#
|
5
|
-
# @see http://
|
7
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3
|
6
8
|
class Password < Base
|
7
9
|
# Not used for this strategy
|
8
10
|
#
|
data/lib/oauth2/version.rb
CHANGED