oauth2 1.4.10 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +97 -28
- data/CONTRIBUTING.md +27 -1
- data/README.md +186 -93
- data/SECURITY.md +5 -5
- data/lib/oauth2/access_token.rb +29 -20
- data/lib/oauth2/authenticator.rb +9 -4
- data/lib/oauth2/client.rb +116 -79
- data/lib/oauth2/error.rb +27 -18
- data/lib/oauth2/response.rb +73 -22
- data/lib/oauth2/snaky_hash.rb +8 -0
- data/lib/oauth2/strategy/assertion.rb +63 -38
- data/lib/oauth2/strategy/auth_code.rb +12 -1
- data/lib/oauth2/strategy/implicit.rb +7 -0
- data/lib/oauth2/version.rb +1 -59
- data/lib/oauth2.rb +19 -1
- metadata +74 -59
- data/lib/oauth2/mac_token.rb +0 -130
data/lib/oauth2/client.rb
CHANGED
@@ -5,6 +5,8 @@ require 'logger'
|
|
5
5
|
|
6
6
|
module OAuth2
|
7
7
|
ConnectionError = Class.new(Faraday::ConnectionFailed)
|
8
|
+
TimeoutError = Class.new(Faraday::TimeoutError)
|
9
|
+
|
8
10
|
# The OAuth2::Client class
|
9
11
|
class Client # rubocop:disable Metrics/ClassLength
|
10
12
|
RESERVED_PARAM_KEYS = %w[headers parse].freeze
|
@@ -22,15 +24,16 @@ module OAuth2
|
|
22
24
|
# @param [Hash] options the options to create the client with
|
23
25
|
# @option options [String] :site the OAuth2 provider site host
|
24
26
|
# @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
|
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)
|
28
30
|
# @option options [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
|
29
31
|
# @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
|
30
32
|
# @option options [FixNum] :max_redirects (5) maximum number of redirects to follow
|
31
33
|
# @option options [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
|
32
34
|
# @option options [Logger] :logger (::Logger.new($stdout)) which logger to use when OAUTH_DEBUG is enabled
|
33
|
-
# @option options [Proc]
|
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+
|
34
37
|
# @yield [builder] The Faraday connection builder
|
35
38
|
def initialize(client_id, client_secret, options = {}, &block)
|
36
39
|
opts = options.dup
|
@@ -38,16 +41,19 @@ module OAuth2
|
|
38
41
|
@secret = client_secret
|
39
42
|
@site = opts.delete(:site)
|
40
43
|
ssl = opts.delete(:ssl)
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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)
|
51
57
|
@options[:connection_opts][:ssl] = ssl if ssl
|
52
58
|
end
|
53
59
|
|
@@ -89,6 +95,9 @@ module OAuth2
|
|
89
95
|
end
|
90
96
|
|
91
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
|
92
101
|
#
|
93
102
|
# @param [Symbol] verb one of :get, :post, :put, :delete
|
94
103
|
# @param [String] url URL path of request
|
@@ -99,20 +108,10 @@ module OAuth2
|
|
99
108
|
# @option opts [Boolean] :raise_errors whether or not to raise an OAuth2::Error on 400+ status
|
100
109
|
# code response for this request. Will default to client option
|
101
110
|
# @option opts [Symbol] :parse @see Response::initialize
|
102
|
-
# @
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
113
|
-
end
|
114
|
-
|
115
|
-
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)
|
116
115
|
|
117
116
|
case response.status
|
118
117
|
when 301, 302, 303, 307
|
@@ -126,7 +125,8 @@ module OAuth2
|
|
126
125
|
end
|
127
126
|
location = response.headers['location']
|
128
127
|
if location
|
129
|
-
|
128
|
+
full_location = response.response.env.url.merge(location)
|
129
|
+
request(verb, full_location, opts)
|
130
130
|
else
|
131
131
|
error = Error.new(response)
|
132
132
|
raise(error, "Got #{response.status} status code, but no Location header was present")
|
@@ -138,7 +138,6 @@ module OAuth2
|
|
138
138
|
error = Error.new(response)
|
139
139
|
raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
|
140
140
|
|
141
|
-
response.error = error
|
142
141
|
response
|
143
142
|
else
|
144
143
|
error = Error.new(response)
|
@@ -148,53 +147,52 @@ module OAuth2
|
|
148
147
|
|
149
148
|
# Initializes an AccessToken by making a request to the token endpoint
|
150
149
|
#
|
151
|
-
# @param params [Hash] a Hash of params for the token endpoint
|
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
|
152
153
|
# @param access_token_opts [Hash] access token options, to pass to the AccessToken object
|
153
|
-
# @param
|
154
|
+
# @param extract_access_token [Proc] proc that extracts the access token from the response (DEPRECATED)
|
155
|
+
# @yield [req] @see Faraday::Connection#run_request
|
154
156
|
# @return [AccessToken] the initialized AccessToken
|
155
|
-
def get_token(params, access_token_opts = {}, extract_access_token =
|
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]
|
156
160
|
params = params.map do |key, value|
|
157
161
|
if RESERVED_PARAM_KEYS.include?(key)
|
158
162
|
[key.to_sym, value]
|
159
163
|
else
|
160
164
|
[key, value]
|
161
165
|
end
|
162
|
-
end
|
163
|
-
|
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
|
+
}
|
164
173
|
|
165
174
|
params = authenticator.apply(params)
|
166
|
-
opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
|
167
175
|
headers = params.delete(:headers) || {}
|
168
176
|
if options[:token_method] == :post
|
169
|
-
|
170
|
-
|
177
|
+
request_opts[:body] = params
|
178
|
+
request_opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
|
171
179
|
else
|
172
|
-
|
173
|
-
|
180
|
+
request_opts[:params] = params
|
181
|
+
request_opts[:headers] = {}
|
174
182
|
end
|
175
|
-
|
183
|
+
request_opts[:headers].merge!(headers)
|
176
184
|
http_method = options[:token_method]
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
+
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)
|
195
195
|
end
|
196
|
-
|
197
|
-
access_token
|
198
196
|
end
|
199
197
|
|
200
198
|
# The Authorization Code strategy
|
@@ -253,13 +251,25 @@ module OAuth2
|
|
253
251
|
end
|
254
252
|
end
|
255
253
|
|
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
254
|
private
|
262
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
|
+
|
263
273
|
# Returns the authenticator object
|
264
274
|
#
|
265
275
|
# @return [Authenticator] the initialized Authenticator
|
@@ -267,26 +277,53 @@ module OAuth2
|
|
267
277
|
Authenticator.new(id, secret, options[:auth_scheme])
|
268
278
|
end
|
269
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
|
+
|
270
307
|
# Builds the access token from the response of the HTTP call
|
271
308
|
#
|
272
309
|
# @return [AccessToken] the initialized AccessToken
|
273
|
-
def build_access_token(response, access_token_opts,
|
274
|
-
|
275
|
-
|
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)
|
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=)
|
285
313
|
end
|
286
314
|
end
|
287
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
|
+
|
288
325
|
def oauth_debug_logging(builder)
|
289
|
-
builder.response :logger, options[:logger], :
|
326
|
+
builder.response :logger, options[:logger], bodies: true if ENV['OAUTH_DEBUG'] == 'true'
|
290
327
|
end
|
291
328
|
end
|
292
329
|
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
|
8
|
-
#
|
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
|
-
|
16
|
+
message_opts = parse_error_description(@code, @description)
|
17
17
|
end
|
18
18
|
|
19
|
-
super(error_message(response.body,
|
19
|
+
super(error_message(response.body, message_opts))
|
20
20
|
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
# @param [String] opts :error_description error description to show first line
|
22
|
+
private
|
23
|
+
|
25
24
|
def error_message(response_body, opts = {})
|
26
|
-
|
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
|
-
|
36
|
+
lines << error_string
|
37
|
+
|
38
|
+
lines.join("\n")
|
39
|
+
end
|
29
40
|
|
30
|
-
|
31
|
-
|
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
|
-
|
44
|
+
error_description = ''
|
45
|
+
error_description += "#{code}: " if code
|
46
|
+
error_description += description if description
|
38
47
|
|
39
|
-
|
48
|
+
{error_description: error_description}
|
40
49
|
end
|
41
50
|
end
|
42
51
|
end
|
data/lib/oauth2/response.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
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 :
|
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
|
-
:
|
17
|
-
:
|
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
|
}
|
@@ -42,12 +39,17 @@ module OAuth2
|
|
42
39
|
# Initializes a Response instance
|
43
40
|
#
|
44
41
|
# @param [Faraday::Response] response The Faraday response instance
|
45
|
-
# @param [
|
46
|
-
# @option opts [Symbol] :parse (:automatic) how to parse the response body. one of :query (for x-www-form-urlencoded),
|
42
|
+
# @param [Symbol] parse (:automatic) how to parse the response body. one of :query (for x-www-form-urlencoded),
|
47
43
|
# :json, or :automatic (determined by Content-Type response header)
|
48
|
-
|
44
|
+
# @param [true, false] snaky (true) Convert @parsed to a snake-case,
|
45
|
+
# indifferent-access OAuth2::SnakyHash, which is a subclass of Hashie::Mash::Rash (from rash_alt gem)?
|
46
|
+
# @param [Hash] options all other options for initializing the instance
|
47
|
+
def initialize(response, parse: :automatic, snaky: true, **options)
|
49
48
|
@response = response
|
50
|
-
@options = {
|
49
|
+
@options = {
|
50
|
+
parse: parse,
|
51
|
+
snaky: snaky,
|
52
|
+
}.merge(options)
|
51
53
|
end
|
52
54
|
|
53
55
|
# The HTTP response headers
|
@@ -65,29 +67,78 @@ module OAuth2
|
|
65
67
|
response.body || ''
|
66
68
|
end
|
67
69
|
|
68
|
-
# The parsed
|
69
|
-
#
|
70
|
-
#
|
70
|
+
# The {#response} {#body} as parsed by {#parser}.
|
71
|
+
#
|
72
|
+
# @return [Object] As returned by {#parser} if it is #call-able.
|
73
|
+
# @return [nil] If the {#parser} is not #call-able.
|
71
74
|
def parsed
|
72
|
-
return
|
75
|
+
return @parsed if defined?(@parsed)
|
76
|
+
|
77
|
+
@parsed =
|
78
|
+
if parser.respond_to?(:call)
|
79
|
+
case parser.arity
|
80
|
+
when 0
|
81
|
+
parser.call
|
82
|
+
when 1
|
83
|
+
parser.call(body)
|
84
|
+
else
|
85
|
+
parser.call(body, response)
|
86
|
+
end
|
87
|
+
end
|
73
88
|
|
74
|
-
@parsed
|
89
|
+
@parsed = OAuth2::SnakyHash.new(@parsed) if options[:snaky] && @parsed.is_a?(Hash)
|
90
|
+
|
91
|
+
@parsed
|
75
92
|
end
|
76
93
|
|
77
94
|
# Attempts to determine the content type of the response.
|
78
95
|
def content_type
|
79
|
-
|
96
|
+
return nil unless response.headers
|
97
|
+
|
98
|
+
((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip.downcase
|
80
99
|
end
|
81
100
|
|
82
|
-
# Determines the parser
|
101
|
+
# Determines the parser (a Proc or other Object which responds to #call)
|
102
|
+
# that will be passed the {#body} (and optional {#response}) to supply
|
103
|
+
# {#parsed}.
|
104
|
+
#
|
105
|
+
# The parser can be supplied as the +:parse+ option in the form of a Proc
|
106
|
+
# (or other Object responding to #call) or a Symbol. In the latter case,
|
107
|
+
# the actual parser will be looked up in {@@parsers} by the supplied Symbol.
|
108
|
+
#
|
109
|
+
# If no +:parse+ option is supplied, the lookup Symbol will be determined
|
110
|
+
# by looking up {#content_type} in {@@content_types}.
|
111
|
+
#
|
112
|
+
# If {#parser} is a Proc, it will be called with no arguments, just
|
113
|
+
# {#body}, or {#body} and {#response}, depending on the Proc's arity.
|
114
|
+
#
|
115
|
+
# @return [Proc, #call] If a parser was found.
|
116
|
+
# @return [nil] If no parser was found.
|
83
117
|
def parser
|
84
|
-
return
|
118
|
+
return @parser if defined?(@parser)
|
119
|
+
|
120
|
+
@parser =
|
121
|
+
if options[:parse].respond_to?(:call)
|
122
|
+
options[:parse]
|
123
|
+
elsif options[:parse]
|
124
|
+
@@parsers[options[:parse].to_sym]
|
125
|
+
end
|
85
126
|
|
86
|
-
@@content_types[content_type]
|
127
|
+
@parser ||= @@parsers[@@content_types[content_type]]
|
87
128
|
end
|
88
129
|
end
|
89
130
|
end
|
90
131
|
|
91
|
-
OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body|
|
92
|
-
|
132
|
+
OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml', 'application/xml']) do |body|
|
133
|
+
next body unless body.respond_to?(:to_str)
|
134
|
+
|
135
|
+
MultiXml.parse(body)
|
136
|
+
end
|
137
|
+
|
138
|
+
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|
|
139
|
+
next body unless body.respond_to?(:to_str)
|
140
|
+
|
141
|
+
body = body.dup.force_encoding(::Encoding::ASCII_8BIT) if body.respond_to?(:force_encoding)
|
142
|
+
|
143
|
+
::JSON.parse(body)
|
93
144
|
end
|
@@ -10,15 +10,22 @@ module OAuth2
|
|
10
10
|
#
|
11
11
|
# Sample usage:
|
12
12
|
# client = OAuth2::Client.new(client_id, client_secret,
|
13
|
-
# :site => 'http://localhost:8080'
|
13
|
+
# :site => 'http://localhost:8080',
|
14
|
+
# :auth_scheme => :request_body)
|
14
15
|
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
16
|
+
# claim_set = {
|
17
|
+
# :iss => "http://localhost:3001",
|
18
|
+
# :aud => "http://localhost:8080/oauth2/token",
|
19
|
+
# :sub => "me@example.com",
|
20
|
+
# :exp => Time.now.utc.to_i + 3600,
|
21
|
+
# }
|
20
22
|
#
|
21
|
-
#
|
23
|
+
# encoding = {
|
24
|
+
# :algorithm => 'HS256',
|
25
|
+
# :key => 'secret_key',
|
26
|
+
# }
|
27
|
+
#
|
28
|
+
# access = client.assertion.get_token(claim_set, encoding)
|
22
29
|
# access.token # actual access_token string
|
23
30
|
# access.get("/api/stuff") # making api calls with access token in header
|
24
31
|
#
|
@@ -32,45 +39,63 @@ module OAuth2
|
|
32
39
|
|
33
40
|
# Retrieve an access token given the specified client.
|
34
41
|
#
|
35
|
-
# @param [Hash]
|
36
|
-
#
|
42
|
+
# @param [Hash] claims the hash representation of the claims that should be encoded as a JWT (JSON Web Token)
|
43
|
+
#
|
44
|
+
# For reading on JWT and claim keys:
|
45
|
+
# @see https://github.com/jwt/ruby-jwt
|
46
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
|
47
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7523#section-3
|
48
|
+
# @see https://www.iana.org/assignments/jwt/jwt.xhtml
|
49
|
+
#
|
50
|
+
# There are many possible claim keys, and applications may ask for their own custom keys.
|
51
|
+
# Some typically required ones:
|
52
|
+
# :iss (issuer)
|
53
|
+
# :aud (audience)
|
54
|
+
# :sub (subject) -- formerly :prn https://datatracker.ietf.org/doc/html/draft-ietf-oauth-json-web-token-06#appendix-F
|
55
|
+
# :exp, (expiration time) -- in seconds, e.g. Time.now.utc.to_i + 3600
|
56
|
+
#
|
57
|
+
# Note that this method does *not* validate presence of those four claim keys indicated as required by RFC 7523.
|
58
|
+
# There are endpoints that may not conform with this RFC, and this gem should still work for those use cases.
|
59
|
+
#
|
60
|
+
# @param [Hash] encoding_opts a hash containing instructions on how the JWT should be encoded
|
61
|
+
# @option algorithm [String] the algorithm with which you would like the JWT to be encoded
|
62
|
+
# @option key [Object] the key with which you would like to encode the JWT
|
37
63
|
#
|
38
|
-
#
|
39
|
-
#
|
64
|
+
# These two options are passed directly to `JWT.encode`. For supported encoding arguments:
|
65
|
+
# @see https://github.com/jwt/ruby-jwt#algorithms-and-usage
|
66
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
|
40
67
|
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
# params :exp, expired at, in seconds, like Time.now.utc.to_i + 3600
|
68
|
+
# The object type of `:key` may depend on the value of `:algorithm`. Sample arguments:
|
69
|
+
# get_token(claim_set, {:algorithm => 'HS256', :key => 'secret_key'})
|
70
|
+
# get_token(claim_set, {:algorithm => 'RS256', :key => OpenSSL::PKCS12.new(File.read('my_key.p12'), 'not_secret')})
|
45
71
|
#
|
46
|
-
# @param [Hash]
|
47
|
-
|
48
|
-
|
49
|
-
|
72
|
+
# @param [Hash] request_opts options that will be used to assemble the request
|
73
|
+
# @option request_opts [String] :scope the url parameter `scope` that may be required by some endpoints
|
74
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7521#section-4.1
|
75
|
+
#
|
76
|
+
# @param [Hash] response_opts this will be merged with the token response to create the AccessToken object
|
77
|
+
# @see the access_token_opts argument to Client#get_token
|
78
|
+
|
79
|
+
def get_token(claims, encoding_opts, request_opts = {}, response_opts = {})
|
80
|
+
assertion = build_assertion(claims, encoding_opts)
|
81
|
+
params = build_request(assertion, request_opts)
|
82
|
+
|
83
|
+
@client.get_token(params, response_opts.merge('refresh_token' => nil))
|
50
84
|
end
|
51
85
|
|
52
|
-
|
53
|
-
|
86
|
+
private
|
87
|
+
|
88
|
+
def build_request(assertion, request_opts = {})
|
54
89
|
{
|
55
|
-
:
|
56
|
-
:
|
57
|
-
|
58
|
-
:scope => params[:scope],
|
59
|
-
}
|
90
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
91
|
+
assertion: assertion,
|
92
|
+
}.merge(request_opts)
|
60
93
|
end
|
61
94
|
|
62
|
-
def build_assertion(
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
:prn => params[:prn],
|
67
|
-
:exp => params[:exp],
|
68
|
-
}
|
69
|
-
if params[:hmac_secret]
|
70
|
-
JWT.encode(claims, params[:hmac_secret], 'HS256')
|
71
|
-
elsif params[:private_key]
|
72
|
-
JWT.encode(claims, params[:private_key], 'RS256')
|
73
|
-
end
|
95
|
+
def build_assertion(claims, encoding_opts)
|
96
|
+
raise ArgumentError.new(message: 'Please provide an encoding_opts hash with :algorithm and :key') if !encoding_opts.is_a?(Hash) || (%i[algorithm key] - encoding_opts.keys).any?
|
97
|
+
|
98
|
+
JWT.encode(claims, encoding_opts[:key], encoding_opts[:algorithm])
|
74
99
|
end
|
75
100
|
end
|
76
101
|
end
|
@@ -17,6 +17,7 @@ module OAuth2
|
|
17
17
|
#
|
18
18
|
# @param [Hash] params additional query parameters for the URL
|
19
19
|
def authorize_url(params = {})
|
20
|
+
assert_valid_params(params)
|
20
21
|
@client.authorize_url(authorize_params.merge(params))
|
21
22
|
end
|
22
23
|
|
@@ -28,8 +29,18 @@ module OAuth2
|
|
28
29
|
# @note that you must also provide a :redirect_uri with most OAuth 2.0 providers
|
29
30
|
def get_token(code, params = {}, opts = {})
|
30
31
|
params = {'grant_type' => 'authorization_code', 'code' => code}.merge(@client.redirection_params).merge(params)
|
32
|
+
params_dup = params.dup
|
33
|
+
params.each_key do |key|
|
34
|
+
params_dup[key.to_s] = params_dup.delete(key) if key.is_a?(Symbol)
|
35
|
+
end
|
31
36
|
|
32
|
-
@client.get_token(
|
37
|
+
@client.get_token(params_dup, opts)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def assert_valid_params(params)
|
43
|
+
raise(ArgumentError, 'client_secret is not allowed in authorize URL query params') if params.key?(:client_secret) || params.key?('client_secret')
|
33
44
|
end
|
34
45
|
end
|
35
46
|
end
|