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.
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 or :post)
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] (DEPRECATED) :extract_access_token proc that extracts the access token from the response
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
- @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)
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
- # @yield [req] The Faraday request
103
- def request(verb, url, opts = {}) # rubocop:disable Metrics/AbcSize
104
- url = connection.build_url(url).to_s
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
- request(verb, location, opts)
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 access_token_class [Class] class of access token for easier subclassing OAuth2::AccessToken
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 = options[:extract_access_token]) # # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity Metrics/AbcSize, Metrics/MethodLength
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
- params = Hash[params]
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
- opts[:body] = params
170
- 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'}
171
179
  else
172
- opts[:params] = params
173
- opts[:headers] = {}
180
+ request_opts[:params] = params
181
+ request_opts[:headers] = {}
174
182
  end
175
- opts[:headers] = opts[:headers].merge(headers)
183
+ request_opts[:headers].merge!(headers)
176
184
  http_method = options[:token_method]
177
- response = request(http_method, token_url, opts)
178
-
179
- access_token = begin
180
- build_access_token(response, access_token_opts, extract_access_token)
181
- rescue StandardError
182
- nil
183
- end
184
-
185
- response_contains_token = access_token || (
186
- response.parsed.is_a?(Hash) &&
187
- (response.parsed['access_token'] || response.parsed['id_token'])
188
- )
189
-
190
- if options[:raise_errors] && !response_contains_token
191
- error = Error.new(response)
192
- raise(error)
193
- elsif !response_contains_token
194
- return nil
185
+ 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, extract_access_token)
274
- parsed_response = response.parsed.dup
275
- return unless parsed_response.is_a?(Hash)
276
-
277
- hash = parsed_response.merge(access_token_opts)
278
-
279
- # Provide backwards compatibility for old AccessToken.form_hash pattern
280
- # Will be deprecated in 2.x
281
- if extract_access_token.is_a?(Class) && extract_access_token.respond_to?(:from_hash)
282
- extract_access_token.from_hash(self, hash)
283
- else
284
- extract_access_token.call(self, hash)
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], :bodies => true if ENV['OAUTH_DEBUG'] == 'true'
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 values include:
8
- # :invalid_request, :invalid_client, :invalid_token, :invalid_grant, :unsupported_grant_type, :invalid_scope
7
+ # standard error codes include:
8
+ # 'invalid_request', 'invalid_client', 'invalid_token', 'invalid_grant', 'unsupported_grant_type', 'invalid_scope'
9
9
  def initialize(response)
10
- response.error = self
11
10
  @response = response
11
+ message_opts = {}
12
12
 
13
13
  if response.parsed.is_a?(Hash)
14
14
  @code = response.parsed['error']
15
15
  @description = response.parsed['error_description']
16
- error_description = "#{@code}: #{@description}"
16
+ message_opts = parse_error_description(@code, @description)
17
17
  end
18
18
 
19
- super(error_message(response.body, :error_description => error_description))
19
+ super(error_message(response.body, message_opts))
20
20
  end
21
21
 
22
- # Makes a error message
23
- # @param [String] response_body response body of request
24
- # @param [String] opts :error_description error description to show first line
22
+ private
23
+
25
24
  def error_message(response_body, opts = {})
26
- message = []
25
+ lines = []
26
+
27
+ lines << opts[:error_description] if opts[:error_description]
28
+
29
+ error_string = if response_body.respond_to?(:encode) && opts[:error_description].respond_to?(:encoding)
30
+ script_encoding = opts[:error_description].encoding
31
+ response_body.encode(script_encoding, invalid: :replace, undef: :replace)
32
+ else
33
+ response_body
34
+ end
27
35
 
28
- opts[:error_description] && (message << opts[:error_description])
36
+ lines << error_string
37
+
38
+ lines.join("\n")
39
+ end
29
40
 
30
- error_message = if opts[:error_description] && opts[:error_description].respond_to?(:encoding)
31
- script_encoding = opts[:error_description].encoding
32
- response_body.encode(script_encoding, :invalid => :replace, :undef => :replace)
33
- else
34
- response_body
35
- end
41
+ def parse_error_description(code, description)
42
+ return {} unless code || description
36
43
 
37
- message << error_message
44
+ error_description = ''
45
+ error_description += "#{code}: " if code
46
+ error_description += description if description
38
47
 
39
- message.join("\n")
48
+ {error_description: error_description}
40
49
  end
41
50
  end
42
51
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'multi_json'
3
+ require 'json'
4
4
  require 'multi_xml'
5
5
  require 'rack'
6
6
 
@@ -8,20 +8,17 @@ module OAuth2
8
8
  # OAuth2::Response class
9
9
  class Response
10
10
  attr_reader :response
11
- attr_accessor :error, :options
11
+ attr_accessor :options
12
12
 
13
13
  # Procs that, when called, will parse a response body according
14
14
  # to the specified format.
15
15
  @@parsers = {
16
- :json => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable Style/RescueModifier
17
- :query => lambda { |body| Rack::Utils.parse_query(body) },
18
- :text => lambda { |body| body },
16
+ query: ->(body) { Rack::Utils.parse_query(body) },
17
+ text: ->(body) { body },
19
18
  }
20
19
 
21
20
  # Content type assignments for various potential HTTP content types.
22
21
  @@content_types = {
23
- 'application/json' => :json,
24
- 'text/javascript' => :json,
25
22
  'application/x-www-form-urlencoded' => :query,
26
23
  'text/plain' => :text,
27
24
  }
@@ -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 [Hash] opts options in which to initialize the instance
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
- def initialize(response, opts = {})
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 = {:parse => :automatic}.merge(opts)
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 response body.
69
- # Will attempt to parse application/x-www-form-urlencoded and
70
- # application/json Content-Type response bodies
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 nil unless @@parsers.key?(parser)
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 ||= @@parsers[parser].call(body)
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
- ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
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 that will be used to supply the content of #parsed
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 options[:parse].to_sym if @@parsers.key?(options[:parse])
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
- MultiXml.parse(body) rescue body # rubocop:disable Style/RescueModifier
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OAuth2
4
+ # Hash which allow assign string key in camel case
5
+ # and query on both camel and snake case
6
+ class SnakyHash < ::Hashie::Mash::Rash
7
+ end
8
+ end
@@ -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
- # params = {:hmac_secret => "some secret",
16
- # # or :private_key => "private key string",
17
- # :iss => "http://localhost:3001",
18
- # :prn => "me@here.com",
19
- # :exp => Time.now.utc.to_i + 3600}
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
- # access = client.assertion.get_token(params)
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] params assertion params
36
- # pass either :hmac_secret or :private_key, but not both.
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
- # params :hmac_secret, secret string.
39
- # params :private_key, private key string.
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
- # params :iss, issuer
42
- # params :aud, audience, optional
43
- # params :prn, principal, current user
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] opts options
47
- def get_token(params = {}, opts = {})
48
- hash = build_request(params)
49
- @client.get_token(hash, opts.merge('refresh_token' => nil))
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
- def build_request(params)
53
- assertion = build_assertion(params)
86
+ private
87
+
88
+ def build_request(assertion, request_opts = {})
54
89
  {
55
- :grant_type => 'assertion',
56
- :assertion_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
57
- :assertion => assertion,
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(params)
63
- claims = {
64
- :iss => params[:iss],
65
- :aud => params[:aud],
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(params, opts)
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