cerner-oauth1a 2.4.0 → 2.5.4

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: 7c216d39c6c458e3987b9b46dbedf8cd58c462fd336d1042d1f7f827e0be71f3
4
+ data.tar.gz: 79166e522691bbac84d2190bad3f824441eb3b2c8448c6547c1874a627703f4e
5
5
  SHA512:
6
- metadata.gz: 28473811816dc086d4eacdd3eff65d0fe5455087f5e1e5c3fb0bfb3e04ba38a819707536e40968ad157c9efe1e8756bbd8f3f87cd620c796fd3da750a247efa7
7
- data.tar.gz: ae958bbf7456257dbef524a1e6073d3f9ce0c7988516f24c18f7011b689d769e86d0340fdbca7de44daf0c1b0bdbcf14768112ab13fd6f3b8573846fe284e63f
6
+ metadata.gz: 298d94d966ff6bed04ab29f8ee8ad3076507e9cc950517eff5bf0fb9c31d15b89924fa5c1fe384b1fd20f16688327b90267178b7bcf80b9958dcd6e0cb2d0b2d
7
+ data.tar.gz: d3e0c95062ed21513a20c47c2115f0385260173f60ed9c7676c320084b787c2488d301f7436330ebdae9f78d044790837c2694197e34ef70f2ba3d769bb61cad
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ # v2.5.4
2
+ Replace invalid usage of `oauth_parameters_rejected` and `oauth_parameters_absent`
3
+ OAuth problem values with correct values `parameter_rejected` and `parameter_absent`
4
+ when reporting certain errors.
5
+
6
+ # v2.5.3
7
+ Use a constant time compare algorithm for checking a signature
8
+
9
+ # v2.5.2
10
+ Adjust `Cerner::OAuth1a::Protocol.parse_www_authenticate_header` to handle parameters
11
+ that are either tokens or quoted strings.
12
+
13
+ # v2.5.1
14
+ Address `instance variable @cache_instance not initialized` warning
15
+
16
+ # v2.5.0
17
+ Add Consumer and Provider support for HMAC-SHA1 signatures.
18
+
19
+ Added a Cerner::OAuth1a::Protocol.percent_encode method.
20
+
21
+ Correctly percent encodes PLAINTEXT signature parts (client shared secret and token
22
+ shared secret) before constructing PLAINTEXT signature.
23
+
1
24
  # v2.4.0
2
25
  Handle nonce and timestamp as optional fields Per
3
26
  https://tools.ietf.org/html/rfc5849#section-3.1, the oauth_timestamp and oauth_nonce
data/NOTICE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2019 Cerner Innovation, Inc.
1
+ Copyright 2020 Cerner Innovation, Inc.
2
2
 
3
3
  Licensed under the Apache License, Version 2.0 (the "License");
4
4
  you may not use this file except in compliance with the License.
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
@@ -149,7 +211,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md)
149
211
 
150
212
  # LICENSE
151
213
 
152
- Copyright 2019 Cerner Innovation, Inc.
214
+ Copyright 2020 Cerner Innovation, Inc.
153
215
 
154
216
  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
155
217
 
@@ -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,46 +365,22 @@ 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, 'parameter_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
- raise OAuthError.new('token missing KeysVersion', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
383
+ raise OAuthError.new('token missing KeysVersion', nil, 'parameter_rejected', 'oauth_token', @realm)
317
384
  end
318
385
 
319
386
  begin
@@ -322,7 +389,7 @@ module Cerner
322
389
  raise OAuthError.new(
323
390
  'token references invalid keys version',
324
391
  nil,
325
- 'oauth_parameters_rejected',
392
+ 'parameter_rejected',
326
393
  'oauth_token',
327
394
  @realm
328
395
  )
@@ -330,29 +397,19 @@ 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, 'parameter_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
- raise OAuthError.new('missing signature', nil, 'oauth_parameters_absent', 'oauth_signature', @realm)
409
+ raise OAuthError.new('missing signature', nil, 'parameter_absent', 'oauth_signature', @realm)
353
410
  end
354
411
  unless hmac_secrets
355
- raise OAuthError.new('missing HMACSecrets', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
412
+ raise OAuthError.new('missing HMACSecrets', nil, 'parameter_rejected', 'oauth_token', @realm)
356
413
  end
357
414
 
358
415
  begin
@@ -361,18 +418,65 @@ module Cerner
361
418
  raise OAuthError.new(
362
419
  "unable to decrypt HMACSecrets: #{e.message}",
363
420
  nil,
364
- 'oauth_parameters_rejected',
421
+ 'parameter_rejected',
365
422
  'oauth_token',
366
423
  @realm
367
424
  )
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 Internal.constant_time_compare(@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