cerner-oauth1a 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 253284a714a5a69821159cef402c2e228c7d747a15f31dc4d8842bb7e44ee6fe
4
- data.tar.gz: da348e1e5cfa763a78afd32e3bd252719670c3feb7bce4cad7995023cee384c4
3
+ metadata.gz: d923d6f574a554d2995de4c00c3a97924a3750f424f1b141accd4eb239401e70
4
+ data.tar.gz: 9f72e25bc46201b3a071b1b34e8450d40fd9db0e29ac02c1ab6153742357cdf9
5
5
  SHA512:
6
- metadata.gz: 28473811816dc086d4eacdd3eff65d0fe5455087f5e1e5c3fb0bfb3e04ba38a819707536e40968ad157c9efe1e8756bbd8f3f87cd620c796fd3da750a247efa7
7
- data.tar.gz: ae958bbf7456257dbef524a1e6073d3f9ce0c7988516f24c18f7011b689d769e86d0340fdbca7de44daf0c1b0bdbcf14768112ab13fd6f3b8573846fe284e63f
6
+ metadata.gz: d752ee31d28626e1bdf135e20347f240b9658025cfffdb46d4e2b409aaba7ec2222ba30263b0aeedce0491168af8ea65dfc267b45f1345e77c6773cbbf767c7b
7
+ data.tar.gz: 848a69c486be5a55861fce17cc8cc9da4102ddda4b9540c37eb19ada65b83628a28c8d9571bdf2ef7b7ed0d0e545b294da4ecd92aed891daeb9221172adf05de
@@ -1,3 +1,11 @@
1
+ # v2.5.0
2
+ Add Consumer and Provider support for HMAC-SHA1 signatures.
3
+
4
+ Added a Cerner::OAuth1a::Protocol.percent_encode method.
5
+
6
+ Correctly percent encodes PLAINTEXT signature parts (client shared secret and token
7
+ shared secret) before constructing PLAINTEXT signature.
8
+
1
9
  # v2.4.0
2
10
  Handle nonce and timestamp as optional fields Per
3
11
  https://tools.ietf.org/html/rfc5849#section-3.1, the oauth_timestamp and oauth_nonce
data/README.md CHANGED
@@ -38,7 +38,53 @@ for implementing a Ruby-based service.
38
38
  # Invoke the API's HTTP endpoint and use the AccessToken to generate an Authorization header
39
39
  response = http.request_get(uri.path, Authorization: access_token.authorization_header)
40
40
 
41
+ ### Consumer HMAC-SHA1 Signature Method
42
+
43
+ The preferred and default signature method is PLAINTEXT, as all communication SHOULD be via TLS. However, if HMAC-SHA1 signatures are necessary, then this can be achieved by constructing AccessTokenAgent as follows:
44
+
45
+ agent = Cerner::OAuth1a::AccessTokenAgent.new(
46
+ access_token_url: 'https://oauth-api.cerner.com/oauth/access',
47
+ consumer_key: 'CONSUMER_KEY',
48
+ consumer_secret: 'CONSUMER_SECRET',
49
+ signature_method: 'HMAC-SHA1'
50
+ )
51
+
52
+ To use the AccessToken requires additional parameters to be passed when constructing the Authorization header. The HTTP method, the URL being invoked and all request parameters. The request parameters should include all parameters passed in the query string and those passed in the body if the Content-Type of the body is `application/x-www-form-urlencoded`. See the specification for more details.
53
+
54
+ #### Consumer HMAC-SHA1 Signature Method Examples
55
+
56
+ GET with no request parameters
57
+
58
+ uri = URI('https://authz-demo-api.cerner.com/me')
59
+ # ...
60
+ authz_header = access_token.authorization_header(fully_qualified_url: uri)
61
+
62
+ GET with request parameters in URL
63
+
64
+ uri = URI('https://authz-demo-api.cerner.com/me?name=value')
65
+ # ...
66
+ authz_header = access_token.authorization_header(fully_qualified_url: uri)
67
+
68
+ POST with request parameters (form post)
69
+
70
+ authz_header = access_token.authorization_header(
71
+ http_method: 'POST'
72
+ fully_qualified_url: 'https://example/path',
73
+ request_params: {
74
+ sort: 'asc',
75
+ field: ['name', 'desc'] # sending the field multiple times
76
+ }
77
+ )
78
+
79
+ PUT with no request parameters (entity body)
80
+
81
+ authz_header = access_token.authorization_header(
82
+ http_method: 'PUT'
83
+ fully_qualified_url: 'https://example/path'
84
+ )
85
+
41
86
  ### Access Token Reuse
87
+
42
88
  Generally, you'll want to use an Access Token more than once. Access Tokens can be reused, but
43
89
  they do expire, so you'll need to acquire new tokens after one expires. All of the expiration
44
90
  information is contained in the AccessToken class and you can easily determine if a token is
@@ -77,6 +123,21 @@ implement that:
77
123
  # (xoauth_principal)
78
124
  consumer_principal = access_token.consumer_principal
79
125
 
126
+ ### Service Provider HMAC-SHA1 Signature Method
127
+
128
+ The preferred and default signature method is PLAINTEXT, as all communication SHOULD be via TLS. However, if HMAC-SHA1 signatures are necessary, then this can be achieved by passing additional informational to the `authenticate` method.
129
+
130
+ begin
131
+ results = access_token.authenticate(
132
+ agent,
133
+ http_method: request.method,
134
+ fully_qualified_url: request.original_url,
135
+ request_params: request.parameters
136
+ )
137
+ rescue OAuthError => e
138
+ # respond with a 401
139
+ end
140
+
80
141
  ## Caching
81
142
 
82
143
  The AccessTokenAgent class provides built-in memory caching. AccessTokens and Keys are cached
@@ -90,6 +151,7 @@ cache to use an implementation that stores the AccessTokens and Keys within Rail
90
151
 
91
152
  ## References
92
153
  * https://wiki.ucern.com/display/public/reference/Cerner%27s+OAuth+Specification
154
+ * https://tools.ietf.org/html/rfc5849
93
155
  * http://oauth.net/core/1.0a
94
156
  * http://oauth.pbwiki.com/ProblemReporting
95
157
  * https://wiki.ucern.com/display/public/reference/Accessing+Cerner%27s+Web+Services+Using+OAuth+1.0a
@@ -6,4 +6,5 @@ require 'cerner/oauth1a/cache'
6
6
  require 'cerner/oauth1a/cache_rails' if defined?(::Rails) && defined?(::Rails.cache)
7
7
  require 'cerner/oauth1a/keys'
8
8
  require 'cerner/oauth1a/protocol'
9
+ require 'cerner/oauth1a/signature'
9
10
  require 'cerner/oauth1a/version'
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cerner/oauth1a/internal'
3
4
  require 'cerner/oauth1a/oauth_error'
4
5
  require 'cerner/oauth1a/protocol'
6
+ require 'cerner/oauth1a/signature'
5
7
  require 'uri'
6
8
 
7
9
  module Cerner
@@ -38,6 +40,7 @@ module Cerner
38
40
  raise OAuthError.new('', nil, 'parameter_absent', missing_params) unless missing_params.empty?
39
41
 
40
42
  AccessToken.new(
43
+ accessor_secret: params[:oauth_accessor_secret],
41
44
  consumer_key: consumer_key,
42
45
  nonce: params[:oauth_nonce],
43
46
  timestamp: params[:oauth_timestamp],
@@ -48,15 +51,18 @@ module Cerner
48
51
  )
49
52
  end
50
53
 
51
- # Returns a String, but may be nil, with the Accessor Secret related to this token.
54
+ # Returns a String, but may be nil, with the Accessor Secret (oauth_accessor_secret) related
55
+ # to this token. Note: nil and empty are considered equivalent.
52
56
  attr_reader :accessor_secret
53
57
  # Returns a String with the Consumer Key (oauth_consumer_key) related to this token.
54
58
  attr_reader :consumer_key
55
59
  # Returns a Time, but may be nil, which represents the moment when this token expires.
56
60
  attr_reader :expires_at
57
- # Returns a String, but may be nil, with the Nonce (oauth_nonce) related to this token.
61
+ # Returns a String, but may be nil, with the Nonce (oauth_nonce) related to this token. This
62
+ # is generally only populated when parsing a token for authentication.
58
63
  attr_reader :nonce
59
- # Returns a Time, but may be nil, which represents the moment when this token was created (oauth_timestamp).
64
+ # Returns a Time, but may be nil, with the Timestamp (oauth_timestamp) related to this token.
65
+ # This is generally only populated when parsing a token for authentication.
60
66
  attr_reader :timestamp
61
67
  # Returns a String with the Token (oauth_token).
62
68
  attr_reader :token
@@ -86,7 +92,7 @@ module Cerner
86
92
  # object responding to to_i that represents the creation
87
93
  # moment as the number of seconds since the epoch.
88
94
  # :token - The required String representing the token.
89
- # :token_secret - The required String representing the token secret.
95
+ # :token_secret - The optional String representing the token secret.
90
96
  # :signature_method - The optional String representing the signature method.
91
97
  # Defaults to PLAINTEXT.
92
98
  # :signature - The optional String representing the signature.
@@ -109,53 +115,122 @@ module Cerner
109
115
  raise ArgumentError, 'token is nil' unless token
110
116
 
111
117
  @accessor_secret = accessor_secret || nil
112
- @authorization_header = nil
113
118
  @consumer_key = consumer_key
114
119
  @consumer_principal = nil
115
- @expires_at = expires_at ? convert_to_time(expires_at) : nil
120
+ @expires_at = expires_at ? Internal.convert_to_time(time: expires_at, name: 'expires_at') : nil
116
121
  @nonce = nonce
117
122
  @signature = signature
118
123
  @signature_method = signature_method || 'PLAINTEXT'
119
- @timestamp = timestamp ? convert_to_time(timestamp) : nil
124
+ @timestamp = timestamp ? Internal.convert_to_time(time: timestamp, name: 'timestamp') : nil
120
125
  @token = token
121
126
  @token_secret = token_secret || nil
122
127
  @realm = realm || nil
123
128
  end
124
129
 
125
130
  # Public: Generates a value suitable for use as an HTTP Authorization header. If #signature is
126
- # nil, then #accessor_secret and #token_secret will be used to build a signature via the
127
- # PLAINTEXT method.
131
+ # nil, then a signature will be generated based on the #signature_method.
132
+ #
133
+ # PLAINTEXT Signature (preferred)
134
+ #
135
+ # When using PLAINTEXT signatures, no additional arguments are necessary. If an oauth_nonce
136
+ # or oauth_timestamp are desired, then the values can be passed via the :nonce and :timestamp
137
+ # keyword arguments. The actual signature will be constructed from the Accessor Secret
138
+ # (#accessor_secret) and the Token Secret (#token_secret).
139
+ #
140
+ # HMAC-SHA1 Signature
141
+ #
142
+ # When using HMAC-SHA1 signatures, access to the HTTP request information is necessary. This
143
+ # requies that additional information is passed via the keyword arguments. The required
144
+ # information includes the HTTP method (see :http_method), the host authority & path (see
145
+ # :fully_qualified_url) and the request parameters (see :fully_qualified_url and
146
+ # :request_params).
147
+ #
148
+ # keywords - The keyword arguments:
149
+ # :nonce - The optional String containing a Nonce to generate the
150
+ # header with HMAC-SHA1 signatures. When nil, a Nonce will
151
+ # be generated.
152
+ # :timestamp - The optional Time or #to_i compliant object containing a
153
+ # Timestamp to generate the header with HMAC-SHA1
154
+ # signatures. When nil, a Timestamp will be generated.
155
+ # :http_method - The optional String or Symbol containing a HTTP Method for
156
+ # constructing the HMAC-SHA1 signature. When nil, the value
157
+ # defualts to 'GET'.
158
+ # :fully_qualified_url - The optional String or URI containing the fully qualified
159
+ # URL of the HTTP API being invoked for constructing the
160
+ # HMAC-SHA1 signature. If the URL contains a query string,
161
+ # the parameters will be extracted and used in addition to
162
+ # the :request_params keyword argument.
163
+ # :request_params - The optional Hash of name/value pairs containing the
164
+ # request parameters of the HTTP API being invoked for
165
+ # constructing the HMAC-SHA1 signature. Parameters passed
166
+ # here will override and augment those passed in the
167
+ # :fully_qualified_url parameter. The parameter names and
168
+ # values MUST be unencoded. See
169
+ # Protocol#parse_url_query_string for help with decoding an
170
+ # encoded query string.
128
171
  #
129
172
  # Returns a String representation of the access token.
130
173
  #
131
174
  # Raises Cerner::OAuth1a::OAuthError if #signature_method is not PLAINTEXT or if a signature
132
175
  # can't be determined.
133
- def authorization_header
134
- return @authorization_header if @authorization_header
135
-
136
- unless @signature_method == 'PLAINTEXT'
137
- raise OAuthError.new('signature_method must be PLAINTEXT', nil, 'signature_method_rejected', nil, @realm)
138
- end
176
+ def authorization_header(
177
+ nonce: nil, timestamp: nil, http_method: 'GET', fully_qualified_url: nil, request_params: nil
178
+ )
179
+ oauth_params = {}
180
+ oauth_params[:oauth_version] = '1.0'
181
+ oauth_params[:oauth_signature_method] = @signature_method
182
+ oauth_params[:oauth_consumer_key] = @consumer_key
183
+ oauth_params[:oauth_nonce] = nonce if nonce
184
+ oauth_params[:oauth_timestamp] = Internal.convert_to_time(time: timestamp, name: 'timestamp').to_i if timestamp
185
+ oauth_params[:oauth_token] = @token
139
186
 
140
187
  if @signature
141
188
  sig = @signature
142
- elsif @accessor_secret && @token_secret
143
- sig = "#{@accessor_secret}&#{@token_secret}"
144
189
  else
145
- raise OAuthError.new('accessor_secret or token_secret is nil', nil, 'parameter_absent', nil, @realm)
190
+ # NOTE: @accessor_secret is always used, but an empty value is allowed and project assumes
191
+ # that nil implies an empty value
192
+
193
+ raise OAuthError.new('token_secret is nil', nil, 'parameter_absent', nil, @realm) unless @token_secret
194
+
195
+ if @signature_method == 'PLAINTEXT'
196
+ sig =
197
+ Signature.sign_via_plaintext(client_shared_secret: @accessor_secret, token_shared_secret: @token_secret)
198
+ elsif @signature_method == 'HMAC-SHA1'
199
+ http_method ||= 'GET' # default to HTTP GET
200
+ request_params ||= {} # default to no request params
201
+ oauth_params[:oauth_nonce] = Internal.generate_nonce unless oauth_params[:oauth_nonce]
202
+ oauth_params[:oauth_timestamp] = Internal.generate_timestamp unless oauth_params[:oauth_timestamp]
203
+
204
+ begin
205
+ fully_qualified_url = Internal.convert_to_http_uri(url: fully_qualified_url, name: 'fully_qualified_url')
206
+ rescue ArgumentError => ae
207
+ raise OAuthError.new(ae.message, nil, 'parameter_absent', nil, @realm)
208
+ end
209
+
210
+ query_params = fully_qualified_url.query ? Protocol.parse_url_query_string(fully_qualified_url.query) : {}
211
+ request_params = query_params.merge(request_params)
212
+
213
+ params = request_params.merge(oauth_params)
214
+ signature_base_string =
215
+ Signature.build_signature_base_string(
216
+ http_method: http_method, fully_qualified_url: fully_qualified_url, params: params
217
+ )
218
+
219
+ sig =
220
+ Signature.sign_via_hmacsha1(
221
+ client_shared_secret: @accessor_secret,
222
+ token_shared_secret: @token_secret,
223
+ signature_base_string: signature_base_string
224
+ )
225
+ else
226
+ raise OAuthError.new('signature_method is invalid', nil, 'signature_method_rejected', nil, @realm)
227
+ end
146
228
  end
147
229
 
148
- tuples = {}
149
- tuples[:realm] = @realm if @realm
150
- tuples[:oauth_version] = '1.0'
151
- tuples[:oauth_signature_method] = @signature_method
152
- tuples[:oauth_signature] = sig
153
- tuples[:oauth_consumer_key] = @consumer_key
154
- tuples[:oauth_nonce] = @nonce if @nonce
155
- tuples[:oauth_timestamp] = @timestamp.tv_sec if @timestamp
156
- tuples[:oauth_token] = @token
157
-
158
- @authorization_header = Protocol.generate_authorization_header(tuples)
230
+ oauth_params[:realm] = @realm if @realm
231
+ oauth_params[:oauth_signature] = sig
232
+
233
+ Protocol.generate_authorization_header(oauth_params)
159
234
  end
160
235
 
161
236
  # Public: Authenticates the #token against the #consumer_key, #signature and side-channel
@@ -166,13 +241,27 @@ module Cerner
166
241
  # access_token_agent - An instance of Cerner::OAuth1a::AccessTokenAgent configured with
167
242
  # appropriate credentials to retrieve secrets via
168
243
  # Cerner::OAuth1a::AccessTokenAgent#retrieve_keys.
244
+ # keywords - The keyword arguments:
245
+ # :http_method - An optional String or Symbol containing an HTTP
246
+ # method name. (default: 'GET')
247
+ # :fully_qualified_url - An optional String or URI that contains the
248
+ # scheme, host, port (optional) and path of a URL.
249
+ # :request_params - An optional Hash of name/value pairs
250
+ # representing the request parameters. The keys
251
+ # and values of the Hash will be assumed to be
252
+ # represented by the value returned from #to_s.
169
253
  #
170
254
  # Returns a Hash (symbolized keys) of any extra parameters within #token (oauth_token),
171
255
  # if authentication succeeds. In most scenarios, the Hash will be empty.
172
256
  #
173
257
  # Raises ArgumentError if access_token_agent is nil
174
258
  # Raises Cerner::OAuth1a::OAuthError with an oauth_problem if authentication fails.
175
- def authenticate(access_token_agent)
259
+ def authenticate(
260
+ access_token_agent,
261
+ http_method: 'GET',
262
+ fully_qualified_url: nil,
263
+ request_params: nil
264
+ )
176
265
  raise ArgumentError, 'access_token_agent is nil' unless access_token_agent
177
266
 
178
267
  if @realm && !access_token_agent.realm_eql?(@realm)
@@ -182,10 +271,6 @@ module Cerner
182
271
  # Set realm to the provider's realm if it's not already set
183
272
  @realm ||= access_token_agent.realm
184
273
 
185
- unless @signature_method == 'PLAINTEXT'
186
- raise OAuthError.new('signature_method must be PLAINTEXT', nil, 'signature_method_rejected', nil, @realm)
187
- end
188
-
189
274
  tuples = Protocol.parse_url_query_string(@token)
190
275
 
191
276
  unless @consumer_key == tuples.delete(:ConsumerKey)
@@ -200,7 +285,13 @@ module Cerner
200
285
  # RSASHA1 param gets consumed in #verify_token, so remove it too
201
286
  tuples.delete(:RSASHA1)
202
287
 
203
- verify_signature(keys, tuples.delete(:HMACSecrets))
288
+ verify_signature(
289
+ keys: keys,
290
+ hmac_secrets: tuples.delete(:HMACSecrets),
291
+ http_method: http_method,
292
+ fully_qualified_url: fully_qualified_url,
293
+ request_params: request_params
294
+ )
204
295
 
205
296
  @consumer_principal = tuples.delete(:"Consumer.Principal")
206
297
 
@@ -222,7 +313,7 @@ module Cerner
222
313
  # if @expires_at is nil, return true now
223
314
  return true unless @expires_at
224
315
 
225
- now = convert_to_time(now)
316
+ now = Internal.convert_to_time(time: now, name: 'now')
226
317
  now.tv_sec >= @expires_at.tv_sec - fudge_sec
227
318
  end
228
319
 
@@ -274,43 +365,19 @@ module Cerner
274
365
 
275
366
  private
276
367
 
277
- # Internal: Used by #initialize and #expired? to convert data into a Time instance.
278
- #
279
- # time - Time or any object with a #to_i the returns an Integer.
280
- #
281
- # Returns a Time instance in the UTC time zone.
282
- def convert_to_time(time)
283
- raise ArgumentError, 'time is nil' unless time
284
-
285
- if time.is_a?(Time)
286
- time.utc
287
- else
288
- Time.at(time.to_i).utc
289
- end
290
- end
291
-
292
368
  # Internal: Used by #authenticate to verify the expiration time.
293
- #
294
- # expires_on - The ExpiresOn parameter of oauth_token
295
- #
296
- # Raises OAuthError if the parameter is invalid or expired
297
369
  def verify_expiration(expires_on)
298
370
  unless expires_on
299
- raise OAuthError.new(
300
- 'token missing ExpiresOn',
301
- nil,
302
- 'oauth_parameters_rejected',
303
- 'oauth_token',
304
- @realm
305
- )
371
+ raise OAuthError.new('token missing ExpiresOn', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
306
372
  end
307
373
 
308
- expires_on = convert_to_time(expires_on)
309
- now = convert_to_time(Time.now)
374
+ expires_on = Internal.convert_to_time(time: expires_on, name: 'expires_on')
375
+ now = Internal.convert_to_time(time: Time.now)
310
376
 
311
377
  raise OAuthError.new('token has expired', nil, 'token_expired', nil, @realm) if now.tv_sec >= expires_on.tv_sec
312
378
  end
313
379
 
380
+ # Internal: Used by #authenticate to load the keys
314
381
  def load_keys(access_token_agent, keys_version)
315
382
  unless keys_version
316
383
  raise OAuthError.new('token missing KeysVersion', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
@@ -330,24 +397,14 @@ module Cerner
330
397
  end
331
398
 
332
399
  # Internal: Used by #authenticate to verify the oauth_token value.
333
- #
334
- # keys - The Keys instance that contains the key used to sign the oauth_token
335
- #
336
- # Raises OAuthError if the parameter is not authentic
337
400
  def verify_token(keys)
338
- unless keys.verify_rsasha1_signature(@token)
339
- raise OAuthError.new('token is not authentic', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
340
- end
401
+ return if keys.verify_rsasha1_signature(@token)
402
+
403
+ raise OAuthError.new('token is not authentic', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
341
404
  end
342
405
 
343
406
  # Internal: Used by #authenticate to verify the request signature.
344
- #
345
- # keys - The Keys instance that contains the key used to encrypt the HMACSecrets
346
- # hmac_secrets - The HMACSecrets parameter of oauth_token
347
- #
348
- # Raises OAuthError if there is no signature, the parameter is invalid or the signature does
349
- # not match the secrets
350
- def verify_signature(keys, hmac_secrets)
407
+ def verify_signature(keys:, hmac_secrets:, http_method:, fully_qualified_url:, request_params:)
351
408
  unless @signature
352
409
  raise OAuthError.new('missing signature', nil, 'oauth_parameters_absent', 'oauth_signature', @realm)
353
410
  end
@@ -368,11 +425,58 @@ module Cerner
368
425
  end
369
426
 
370
427
  secrets_parts = Protocol.parse_url_query_string(secrets)
371
- expected_signature = "#{secrets_parts[:ConsumerSecret]}&#{secrets_parts[:TokenSecret]}"
372
428
 
373
- unless @signature == expected_signature
374
- raise OAuthError.new('signature is not valid', nil, 'signature_invalid', nil, @realm)
429
+ if @signature_method == 'PLAINTEXT'
430
+ expected_signature =
431
+ Signature.sign_via_plaintext(
432
+ client_shared_secret: secrets_parts[:ConsumerSecret], token_shared_secret: secrets_parts[:TokenSecret]
433
+ )
434
+ elsif @signature_method == 'HMAC-SHA1'
435
+ http_method ||= 'GET' # default to HTTP GET
436
+ request_params ||= {} # default to no request params
437
+ oauth_params = {
438
+ oauth_version: '1.0', # assumes version is present
439
+ oauth_signature_method: 'HMAC-SHA1',
440
+ oauth_consumer_key: @consumer_key,
441
+ oauth_nonce: @nonce,
442
+ oauth_timestamp: @timestamp.to_i,
443
+ oauth_token: @token
444
+ }
445
+
446
+ begin
447
+ fully_qualified_url = Internal.convert_to_http_uri(url: fully_qualified_url, name: 'fully_qualified_url')
448
+ rescue ArgumentError => ae
449
+ raise OAuthError.new(ae.message, nil, 'parameter_absent', nil, @realm)
450
+ end
451
+
452
+ query_params = fully_qualified_url.query ? Protocol.parse_url_query_string(fully_qualified_url.query) : {}
453
+ request_params = query_params.merge(request_params)
454
+
455
+ params = request_params.merge(oauth_params)
456
+ signature_base_string =
457
+ Signature.build_signature_base_string(
458
+ http_method: http_method, fully_qualified_url: fully_qualified_url, params: params
459
+ )
460
+
461
+ expected_signature =
462
+ Signature.sign_via_hmacsha1(
463
+ client_shared_secret: secrets_parts[:ConsumerSecret],
464
+ token_shared_secret: secrets_parts[:TokenSecret],
465
+ signature_base_string: signature_base_string
466
+ )
467
+ else
468
+ raise OAuthError.new(
469
+ 'signature_method must be PLAINTEXT or HMAC-SHA1',
470
+ nil,
471
+ 'signature_method_rejected',
472
+ nil,
473
+ @realm
474
+ )
375
475
  end
476
+
477
+ return if @signature == expected_signature
478
+
479
+ raise OAuthError.new('signature is not valid', nil, 'signature_invalid', nil, @realm)
376
480
  end
377
481
  end
378
482
  end
@@ -5,10 +5,12 @@ require 'cerner/oauth1a/access_token'
5
5
  require 'cerner/oauth1a/keys'
6
6
  require 'cerner/oauth1a/oauth_error'
7
7
  require 'cerner/oauth1a/cache'
8
+ require 'cerner/oauth1a/internal'
8
9
  require 'cerner/oauth1a/protocol'
10
+ require 'cerner/oauth1a/signature'
9
11
  require 'cerner/oauth1a/version'
10
12
  require 'json'
11
- require 'net/https'
13
+ require 'net/http'
12
14
  require 'securerandom'
13
15
  require 'uri'
14
16
 
@@ -70,9 +72,11 @@ module Cerner
70
72
  # realm that's extracted from :access_token_url. If nil,
71
73
  # this will be initalized with the DEFAULT_REALM_ALIASES.
72
74
  # (optional, default: nil)
75
+ # :signature_method - A String to set the signature method to use. MUST be
76
+ # PLAINTEXT or HMAC-SHA1. (optional, default: 'PLAINTEXT')
73
77
  #
74
78
  # Raises ArgumentError if access_token_url, consumer_key or consumer_key is nil; if
75
- # access_token_url is an invalid URI.
79
+ # access_token_url is an invalid URI; if signature_method is invalid.
76
80
  def initialize(
77
81
  access_token_url:,
78
82
  consumer_key:,
@@ -81,7 +85,8 @@ module Cerner
81
85
  read_timeout: 5,
82
86
  cache_keys: true,
83
87
  cache_access_tokens: true,
84
- realm_aliases: nil
88
+ realm_aliases: nil,
89
+ signature_method: 'PLAINTEXT'
85
90
  )
86
91
  raise ArgumentError, 'consumer_key is nil' unless consumer_key
87
92
  raise ArgumentError, 'consumer_secret is nil' unless consumer_secret
@@ -89,7 +94,7 @@ module Cerner
89
94
  @consumer_key = consumer_key
90
95
  @consumer_secret = consumer_secret
91
96
 
92
- @access_token_url = convert_to_http_uri(access_token_url)
97
+ @access_token_url = Internal.convert_to_http_uri(url: access_token_url, name: 'access_token_url')
93
98
  @realm = Protocol.realm_for(@access_token_url)
94
99
  @realm_aliases = realm_aliases
95
100
  @realm_aliases ||= DEFAULT_REALM_ALIASES[@realm]
@@ -99,6 +104,9 @@ module Cerner
99
104
 
100
105
  @keys_cache = cache_keys ? Cache.instance : nil
101
106
  @access_token_cache = cache_access_tokens ? Cache.instance : nil
107
+
108
+ @signature_method = signature_method || 'PLAINTEXT'
109
+ raise ArgumentError, 'signature_method is invalid' unless Signature::METHODS.include?(@signature_method)
102
110
  end
103
111
 
104
112
  # Public: Retrieves the service provider keys from the configured Access Token service endpoint
@@ -107,7 +115,7 @@ module Cerner
107
115
  #
108
116
  # keys_version - The version identifier of the keys to retrieve. This corresponds to the
109
117
  # KeysVersion parameter of the oauth_token.
110
- # keywords - The additional, optional keyword arguments for this method
118
+ # keywords - The keyword arguments:
111
119
  # :ignore_cache - A flag for indicating that the cache should be ignored and a
112
120
  # new Access Token should be retrieved.
113
121
  #
@@ -135,7 +143,7 @@ module Cerner
135
143
  # This method will use the #generate_accessor_secret, #generate_nonce and #generate_timestamp methods to
136
144
  # interact with the service, which can be overridden via a sub-class, if desired.
137
145
  #
138
- # keywords - The additional, optional keyword arguments for this method
146
+ # keywords - The keyword arguments:
139
147
  # :principal - An optional principal identifier, which is passed via the
140
148
  # xoauth_principal protocol parameter.
141
149
  # :ignore_cache - A flag for indicating that the cache should be ignored and a new
@@ -147,19 +155,20 @@ module Cerner
147
155
  # Raises StandardError sub-classes for any issues interacting with the service, such as networking issues.
148
156
  def retrieve(principal: nil, ignore_cache: false)
149
157
  cache_key = "#{@consumer_key}&#{principal}"
158
+
150
159
  if @access_token_cache && !ignore_cache
151
160
  cache_entry = @access_token_cache.get('cerner-oauth1a/access-tokens', cache_key)
152
161
  return cache_entry.value if cache_entry
153
162
  end
154
163
 
155
164
  # generate token request info
156
- nonce = generate_nonce
157
165
  timestamp = generate_timestamp
158
166
  accessor_secret = generate_accessor_secret
159
167
 
160
- request = retrieve_prepare_request(timestamp, nonce, accessor_secret, principal)
168
+ request = retrieve_prepare_request(timestamp: timestamp, accessor_secret: accessor_secret, principal: principal)
161
169
  response = http_client.request(request)
162
- access_token = retrieve_handle_response(response, timestamp, nonce, accessor_secret)
170
+ access_token =
171
+ retrieve_handle_response(response: response, timestamp: timestamp, accessor_secret: accessor_secret)
163
172
  @access_token_cache&.put('cerner-oauth1a/access-tokens', cache_key, Cache::AccessTokenEntry.new(access_token))
164
173
  access_token
165
174
  end
@@ -175,14 +184,14 @@ module Cerner
175
184
  #
176
185
  # Returns a String containing the nonce.
177
186
  def generate_nonce
178
- SecureRandom.hex
187
+ Internal.generate_nonce
179
188
  end
180
189
 
181
190
  # Public: Generate a Timestamp for invocations of the Access Token service.
182
191
  #
183
192
  # Returns an Integer representing the number of seconds since the epoch.
184
193
  def generate_timestamp
185
- Time.now.to_i
194
+ Internal.generate_timestamp
186
195
  end
187
196
 
188
197
  # Public: Determines if the passed realm is equivalent to the configured
@@ -224,46 +233,41 @@ module Cerner
224
233
  http
225
234
  end
226
235
 
227
- # Internal: Convert an Access Token URL into a URI with some verification checks
228
- #
229
- # access_token_url - A String URL or a URI instance
230
- # Returns a URI::HTTP or URI::HTTPS
231
- #
232
- # Raises ArgumentError if access_token_url is nil, invalid or not an HTTP/HTTPS URI
233
- def convert_to_http_uri(access_token_url)
234
- raise ArgumentError, 'access_token_url is nil' unless access_token_url
235
-
236
- if access_token_url.is_a?(URI)
237
- uri = access_token_url
238
- else
239
- begin
240
- uri = URI(access_token_url)
241
- rescue URI::InvalidURIError
242
- # raise argument error with cause
243
- raise ArgumentError, 'access_token_url is invalid'
244
- end
245
- end
246
-
247
- raise ArgumentError, 'access_token_url must be an HTTP or HTTPS URI' unless uri.is_a?(URI::HTTP)
248
-
249
- uri
250
- end
251
-
252
236
  # Internal: Prepare a request for #retrieve
253
- def retrieve_prepare_request(timestamp, nonce, accessor_secret, principal)
237
+ def retrieve_prepare_request(accessor_secret:, timestamp:, principal: nil)
254
238
  # construct a POST request
255
239
  request = Net::HTTP::Post.new(@access_token_url)
256
240
  # setup the data to construct the POST's message
257
- params = [
258
- [:oauth_consumer_key, @consumer_key],
259
- [:oauth_signature_method, 'PLAINTEXT'],
260
- [:oauth_version, '1.0'],
261
- [:oauth_timestamp, timestamp],
262
- [:oauth_nonce, nonce],
263
- [:oauth_signature, "#{@consumer_secret}&"],
264
- [:oauth_accessor_secret, accessor_secret]
265
- ]
266
- params << [:xoauth_principal, principal.to_s] if principal
241
+ params = {
242
+ oauth_consumer_key: Protocol.percent_encode(@consumer_key),
243
+ oauth_signature_method: @signature_method,
244
+ oauth_version: '1.0',
245
+ oauth_accessor_secret: accessor_secret
246
+ }
247
+ params[:xoauth_principal] = principal.to_s if principal
248
+
249
+ if @signature_method == 'PLAINTEXT'
250
+ sig = Signature.sign_via_plaintext(client_shared_secret: @consumer_secret, token_shared_secret: '')
251
+ elsif @signature_method == 'HMAC-SHA1'
252
+ params[:oauth_timestamp] = timestamp
253
+ params[:oauth_nonce] = generate_nonce
254
+ signature_base_string =
255
+ Signature.build_signature_base_string(
256
+ http_method: 'POST', fully_qualified_url: @access_token_url, params: params
257
+ )
258
+ sig =
259
+ Signature.sign_via_hmacsha1(
260
+ client_shared_secret: @consumer_secret,
261
+ token_shared_secret: '',
262
+ signature_base_string: signature_base_string
263
+ )
264
+ else
265
+ raise OAuthError.new('signature_method is invalid', nil, 'signature_method_rejected', nil, @realm)
266
+ end
267
+
268
+ params[:oauth_signature] = sig
269
+
270
+ params = params.map { |n, v| [n, v] }
267
271
  # set the POST's body as a URL form-encoded string
268
272
  request.set_form(params, MIME_WWW_FORM_URL_ENCODED, charset: 'UTF-8')
269
273
  request['Accept'] = MIME_WWW_FORM_URL_ENCODED
@@ -273,22 +277,22 @@ module Cerner
273
277
  end
274
278
 
275
279
  # Internal: Handle a response for #retrieve
276
- def retrieve_handle_response(response, timestamp, nonce, accessor_secret)
280
+ def retrieve_handle_response(response:, timestamp:, accessor_secret:)
277
281
  case response
278
282
  when Net::HTTPSuccess
279
283
  # Parse the HTTP response and convert it into a Symbol-keyed Hash
280
284
  tuples = Protocol.parse_url_query_string(response.body)
281
285
  # Use the parsed response to construct the AccessToken
282
- access_token = AccessToken.new(
283
- accessor_secret: accessor_secret,
284
- consumer_key: @consumer_key,
285
- expires_at: timestamp + tuples[:oauth_expires_in].to_i,
286
- nonce: nonce,
287
- timestamp: timestamp,
288
- token: tuples[:oauth_token],
289
- token_secret: tuples[:oauth_token_secret],
290
- realm: @realm
291
- )
286
+ access_token =
287
+ AccessToken.new(
288
+ accessor_secret: accessor_secret,
289
+ consumer_key: @consumer_key,
290
+ expires_at: timestamp + tuples[:oauth_expires_in].to_i,
291
+ token: tuples[:oauth_token],
292
+ token_secret: tuples[:oauth_token_secret],
293
+ signature_method: @signature_method,
294
+ realm: @realm
295
+ )
292
296
  access_token
293
297
  else
294
298
  # Extract any OAuth Problems reported in the response
@@ -300,10 +304,11 @@ module Cerner
300
304
 
301
305
  # Internal: Prepare a request for #retrieve_keys
302
306
  def retrieve_keys_prepare_request(keys_version)
303
- request = Net::HTTP::Get.new(URI("#{@access_token_url}/keys/#{keys_version}"))
307
+ keys_url = URI("#{@access_token_url}/keys/#{keys_version}")
308
+ request = Net::HTTP::Get.new(keys_url)
304
309
  request['Accept'] = 'application/json'
305
310
  request['User-Agent'] = user_agent_string
306
- request['Authorization'] = retrieve.authorization_header
311
+ request['Authorization'] = retrieve.authorization_header(fully_qualified_url: keys_url)
307
312
  request
308
313
  end
309
314
 
@@ -319,9 +324,7 @@ module Cerner
319
324
  raise OAuthError.new('RSA public key retrieved was invalid', nil, nil, nil, @realm) unless rsa_key
320
325
 
321
326
  Keys.new(
322
- version: keys_version,
323
- aes_secret_key: Base64.decode64(aes_key),
324
- rsa_public_key: Base64.decode64(rsa_key)
327
+ version: keys_version, aes_secret_key: Base64.decode64(aes_key), rsa_public_key: Base64.decode64(rsa_key)
325
328
  )
326
329
  else
327
330
  # Extract any OAuth Problems reported in the response
@@ -6,14 +6,14 @@ module Cerner
6
6
  class Cache
7
7
  @cache_instance_lock = Mutex.new
8
8
 
9
+ # Internal: Sets the singleton instance.
9
10
  def self.instance=(cache_impl)
10
11
  raise ArgumentError, 'cache_impl must not be nil' unless cache_impl
11
12
 
12
- @cache_instance_lock.synchronize do
13
- @cache_instance = cache_impl
14
- end
13
+ @cache_instance_lock.synchronize { @cache_instance = cache_impl }
15
14
  end
16
15
 
16
+ # Internal: Gets the singleton instance.
17
17
  def self.instance
18
18
  @cache_instance_lock.synchronize do
19
19
  return @cache_instance if @cache_instance
@@ -27,12 +27,14 @@ module Cerner
27
27
  attr_reader :value
28
28
  attr_reader :expires_in
29
29
 
30
+ # Internal: Constructs an instance.
30
31
  def initialize(keys, expires_in)
31
32
  @value = keys
32
33
  @expires_in = expires_in
33
34
  @expires_at = Time.now.utc.to_i + @expires_in
34
35
  end
35
36
 
37
+ # Internal: Check if the entry is expired.
36
38
  def expired?(now)
37
39
  @expires_at <= now
38
40
  end
@@ -42,25 +44,29 @@ module Cerner
42
44
  class AccessTokenEntry
43
45
  attr_reader :value
44
46
 
47
+ # Internal: Constructs an instance.
45
48
  def initialize(access_token)
46
49
  @value = access_token
47
50
  end
48
51
 
52
+ # Internal: Returns the number of seconds until the entry expires.
49
53
  def expires_in
50
54
  @value.expires_at.to_i - Time.now.utc.to_i
51
55
  end
52
56
 
57
+ # Internal: Check if the entry is expired.
53
58
  def expired?(now)
54
59
  @value.expired?(now: now)
55
60
  end
56
61
  end
57
62
 
58
- ONE_HOUR = 3600
63
+ ONE_HOUR = 3_600
59
64
  TWENTY_FOUR_HOURS = 24 * ONE_HOUR
60
65
 
61
66
  # Internal: The default implementation of the Cerner::OAuth1a::Cache interface.
62
67
  # This implementation just maintains a capped list of entries in memory.
63
68
  class DefaultCache < Cerner::OAuth1a::Cache
69
+ # Internal: Constructs an instance.
64
70
  def initialize(max:)
65
71
  super()
66
72
  @max = max
@@ -68,6 +74,7 @@ module Cerner
68
74
  @entries = {}
69
75
  end
70
76
 
77
+ # Internal: Puts an entry into the cache.
71
78
  def put(namespace, key, entry)
72
79
  @lock.synchronize do
73
80
  now = Time.now.utc.to_i
@@ -77,6 +84,7 @@ module Cerner
77
84
  end
78
85
  end
79
86
 
87
+ # Internal: Gets an entry from the cache.
80
88
  def get(namespace, key)
81
89
  @lock.synchronize do
82
90
  prune_expired(Time.now.utc.to_i)
@@ -28,13 +28,7 @@ module Cerner
28
28
  # key - The key for the cache entries, which is qualified by namespace.
29
29
  # entry - The entry to be stored in the cache.
30
30
  def put(namespace, key, entry)
31
- @cache.write(
32
- key,
33
- entry,
34
- namespace: namespace,
35
- expires_in: entry.expires_in,
36
- race_condition_ttl: 5
37
- )
31
+ @cache.write(key, entry, namespace: namespace, expires_in: entry.expires_in, race_condition_ttl: 5)
38
32
  end
39
33
 
40
34
  # Internal: Retrieves the entry, if available, from the cache store.
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'uri'
5
+
6
+ module Cerner
7
+ module OAuth1a
8
+ # Internal: Internal utility methods
9
+ module Internal
10
+ # Internal: Convert a time value into a Time instance.
11
+ #
12
+ # keywords - The keyword arguments:
13
+ # :time - Time or any object with a #to_i that returns an Integer.
14
+ # :name - The parameter name of the data for invoking methods.
15
+ #
16
+ # Returns a Time instance in the UTC time zone.
17
+ def self.convert_to_time(time:, name: 'time')
18
+ raise ArgumentError, "#{name} is nil" unless time
19
+
20
+ time.is_a?(Time) ? time.utc : Time.at(time.to_i).utc
21
+ end
22
+
23
+ # Internal: Convert an fully qualified URL String into a URI with some verification checks
24
+ #
25
+ # keywords - The keyword arguments:
26
+ # :url - A String or a URI instance to convert to a URI instance.
27
+ # :name - The parameter name of the URL for invoking methods.
28
+ #
29
+ # Returns a URI::HTTP or URI::HTTPS
30
+ #
31
+ # Raises ArgumentError if url is nil, invalid or not an HTTP/HTTPS URI
32
+ def self.convert_to_http_uri(url:, name: 'url')
33
+ raise ArgumentError, "#{name} is nil" unless url
34
+
35
+ if url.is_a?(URI)
36
+ uri = url
37
+ else
38
+ begin
39
+ uri = URI(url)
40
+ rescue URI::InvalidURIError
41
+ # raise argument error with cause
42
+ raise ArgumentError, "#{name} is invalid"
43
+ end
44
+ end
45
+
46
+ raise ArgumentError, "#{name} must be an HTTP or HTTPS URI" unless uri.is_a?(URI::HTTP)
47
+
48
+ uri
49
+ end
50
+
51
+ # Internal: Generate a Nonce for invocations of the Access Token service.
52
+ #
53
+ # Returns a String containing the nonce.
54
+ def self.generate_nonce
55
+ SecureRandom.hex
56
+ end
57
+
58
+ # Internal: Generate a Timestamp for invocations of the Access Token service.
59
+ #
60
+ # Returns an Integer representing the number of seconds since the epoch.
61
+ def self.generate_timestamp
62
+ Time.now.to_i
63
+ end
64
+ end
65
+ end
66
+ end
@@ -6,6 +6,18 @@ module Cerner
6
6
  module OAuth1a
7
7
  # Public: OAuth 1.0a protocol utilities.
8
8
  module Protocol
9
+ # Public: Encodes the passed text using the percent encoding variant described in the OAuth
10
+ # 1.0a specification.
11
+ #
12
+ # Reference: https://tools.ietf.org/html/rfc5849#section-3.6
13
+ #
14
+ # text - A String containing the text to encode.
15
+ #
16
+ # Returns a String that has been encoded.
17
+ def self.percent_encode(text)
18
+ URI.encode_www_form_component(text).gsub('+', '%20')
19
+ end
20
+
9
21
  # Public: Parses a URL-encoded query string into a Hash with symbolized keys.
10
22
  #
11
23
  # query - String containing a URL-encoded query string to parse.
@@ -73,17 +85,12 @@ module Cerner
73
85
  #
74
86
  # Returns the String containing the generated value or nil if params is nil or empty.
75
87
  def self.generate_authorization_header(params)
76
- return nil unless params && !params.empty?
88
+ return unless params && !params.empty?
77
89
 
78
90
  realm = "realm=\"#{params.delete(:realm)}\"" if params[:realm]
79
- realm += ', ' if realm && !params.empty?
80
-
81
- encoded_params =
82
- params.map do |k, v|
83
- k = URI.encode_www_form_component(k).gsub('+', '%20')
84
- v = URI.encode_www_form_component(v).gsub('+', '%20')
85
- "#{k}=\"#{v}\""
86
- end
91
+ realm += ',' if realm && !params.empty?
92
+
93
+ encoded_params = params.map { |k, v| "#{percent_encode(k)}=\"#{percent_encode(v)}\"" }
87
94
 
88
95
  "OAuth #{realm}#{encoded_params.join(',')}"
89
96
  end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'cerner/oauth1a/protocol'
5
+ require 'openssl'
6
+ require 'uri'
7
+
8
+ module Cerner
9
+ module OAuth1a
10
+ # Public: OAuth 1.0a signature utilities.
11
+ module Signature
12
+ METHODS = ['PLAINTEXT', 'HMAC-SHA1'].freeze
13
+
14
+ # Public: Creates a PLAINTEXT signature.
15
+ #
16
+ # Reference: https://tools.ietf.org/html/rfc5849#section-3.4.4
17
+ #
18
+ # keywords - The keyword arguments:
19
+ # :client_shared_secret - Either the Accessor Secret or the Consumer Secret.
20
+ # :token_shared_secret - The Token Secret.
21
+ #
22
+ # Returns a String containing the signature.
23
+ def self.sign_via_plaintext(client_shared_secret:, token_shared_secret:)
24
+ client_shared_secret = Protocol.percent_encode(client_shared_secret)
25
+ token_shared_secret = Protocol.percent_encode(token_shared_secret)
26
+ "#{client_shared_secret}&#{token_shared_secret}"
27
+ end
28
+
29
+ # Public: Creates a HMAC-SHA1 signature.
30
+ #
31
+ # Reference: https://tools.ietf.org/html/rfc5849#section-3.4.2
32
+ #
33
+ # keywords - The keyword arguments:
34
+ # :client_shared_secret - Either the Accessor Secret or the Consumer Secret.
35
+ # :token_shared_secret - The Token Secret.
36
+ # :signature_base_string - The Signature Base String to sign. See
37
+ # Signature.build_signature_base_string.
38
+ #
39
+ # Returns a String containing the signature.
40
+ def self.sign_via_hmacsha1(client_shared_secret:, token_shared_secret:, signature_base_string:)
41
+ client_shared_secret = Protocol.percent_encode(client_shared_secret)
42
+ token_shared_secret = Protocol.percent_encode(token_shared_secret)
43
+ signature_key = "#{client_shared_secret}&#{token_shared_secret}"
44
+ signature = OpenSSL::HMAC.digest('sha1', signature_key, signature_base_string)
45
+ encoded_signature = Base64.strict_encode64(signature)
46
+ encoded_signature
47
+ end
48
+
49
+ # Public: Normalizes a text value as an HTTP method name for use in constructing a Signature
50
+ # Base String.
51
+ #
52
+ # Reference https://tools.ietf.org/html/rfc5849#section-3.4.1.1
53
+ #
54
+ # http_method - A String or Symbol containing an HTTP method name.
55
+ #
56
+ # Returns the normalized value as a String.
57
+ #
58
+ # Raises ArgumentError if http_method is nil.
59
+ def self.normalize_http_method(http_method)
60
+ raise ArgumentError, 'http_method is nil' unless http_method
61
+
62
+ # accepts Symbol or String
63
+ Protocol.percent_encode(http_method.to_s.upcase)
64
+ end
65
+
66
+ # Public: Normalizes a fully qualified URL for use as the Base String URI in constructing a
67
+ # Signature Base String.
68
+ #
69
+ # Reference https://tools.ietf.org/html/rfc5849#section-3.4.1.2
70
+ #
71
+ # fully_qualified_url - A String or URI that contains the scheme, host, port (optional) and
72
+ # path of a URL.
73
+ #
74
+ # Returns the normalized value as a String.
75
+ #
76
+ # Raises ArgumentError if fully_qualified_url is nil.
77
+ def self.normalize_base_string_uri(fully_qualified_url)
78
+ raise ArgumentError, 'fully_qualified_url is nil' unless fully_qualified_url
79
+
80
+ u = fully_qualified_url.is_a?(URI) ? fully_qualified_url : URI(fully_qualified_url)
81
+
82
+ Protocol.percent_encode(URI("#{u.scheme}://#{u.host}:#{u.port}#{u.path}").to_s)
83
+ end
84
+
85
+ # Public: Normalizes the parameters (query string and OAuth parameters) for use as the
86
+ # request parameters in constructing a Signature Base String.
87
+ #
88
+ # Reference: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
89
+ #
90
+ # params - A Hash of name/value pairs representing the parameters. The keys and values of the
91
+ # Hash will be assumed to be represented by the value returned from #to_s.
92
+ #
93
+ # Returns the normalized value as a String.
94
+ #
95
+ # Raises ArgumentError if params is nil.
96
+ def self.normalize_parameters(params)
97
+ raise ArgumentError, 'params is nil' unless params
98
+
99
+ encoded_params =
100
+ params.map do |name, value|
101
+ result = [Protocol.percent_encode(name.to_s), nil]
102
+ result[1] =
103
+ if value.is_a?(Array)
104
+ value = value.map { |e| Protocol.percent_encode(e.to_s) }
105
+ value.sort
106
+ else
107
+ Protocol.percent_encode(value.to_s)
108
+ end
109
+ result
110
+ end
111
+
112
+ sorted_params = encoded_params.sort_by { |name, _| name }
113
+
114
+ exploded_params =
115
+ sorted_params.map do |pair|
116
+ name = pair[0]
117
+ value = pair[1]
118
+ if value.is_a?(Array)
119
+ value.map { |e| "#{name}=#{e}" }
120
+ else
121
+ "#{name}=#{value}"
122
+ end
123
+ end
124
+ exploded_params.flatten!
125
+
126
+ joined_params = exploded_params.join('&')
127
+ Protocol.percent_encode(joined_params)
128
+ end
129
+
130
+ # Public: Builds a Signature Base String.
131
+ #
132
+ # keywords - The keyword arguments:
133
+ # :http_method - A String or Symbol containing an HTTP method name.
134
+ # :fully_qualified_url - A String or URI that contains the scheme, host, port
135
+ # (optional) and path of a URL.
136
+ # :params - A Hash of name/value pairs representing the parameters.
137
+ # The keys and values of the Hash will be assumed to be
138
+ # represented by the value returned from #to_s.
139
+ #
140
+ # Returns the Signature Base String as a String.
141
+ #
142
+ # Raises ArgumentError if http_method, fully_qualified_url or params is nil.
143
+ def self.build_signature_base_string(http_method:, fully_qualified_url:, params:)
144
+ raise ArgumentError, 'http_method is nil' unless http_method
145
+ raise ArgumentError, 'fully_qualified_url is nil' unless fully_qualified_url
146
+ raise ArgumentError, 'params is nil' unless params
147
+
148
+ parts = [
149
+ normalize_http_method(http_method),
150
+ normalize_base_string_uri(fully_qualified_url),
151
+ normalize_parameters(params)
152
+ ]
153
+ parts.join('&')
154
+ end
155
+ end
156
+ end
157
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cerner
4
4
  module OAuth1a
5
- VERSION = '2.4.0'
5
+ VERSION = '2.5.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cerner-oauth1a
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Beyer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-12-05 00:00:00.000000000 Z
11
+ date: 2019-12-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  A minimal dependency library for interacting with a Cerner OAuth 1.0a Access
@@ -30,9 +30,11 @@ files:
30
30
  - lib/cerner/oauth1a/access_token_agent.rb
31
31
  - lib/cerner/oauth1a/cache.rb
32
32
  - lib/cerner/oauth1a/cache_rails.rb
33
+ - lib/cerner/oauth1a/internal.rb
33
34
  - lib/cerner/oauth1a/keys.rb
34
35
  - lib/cerner/oauth1a/oauth_error.rb
35
36
  - lib/cerner/oauth1a/protocol.rb
37
+ - lib/cerner/oauth1a/signature.rb
36
38
  - lib/cerner/oauth1a/version.rb
37
39
  homepage: http://github.com/cerner/cerner-oauth1a
38
40
  licenses: