cerner-oauth1a 2.3.0 → 2.5.3

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: 97551b442a5cf8e81197726dd7a076fd49ed7e33c5fd2bbf87e27e6368abea4b
4
- data.tar.gz: de3ec59f5a000715440492c550bebe4f9d3490ce53fd9d88490a08f7539d7208
3
+ metadata.gz: a554af1b8a4a44ff14a05c9fb71517046cdb1a5bf677b5a4792eaa7d01423b83
4
+ data.tar.gz: 3f16a0c5584bae17e6edfcecb277c92d04da581fcc883f687b91719089543b8e
5
5
  SHA512:
6
- metadata.gz: a98454061208707ddab9eb7a79b867f8acbcd88fa2d42c9e68411a04fa645004e20d282e39efec0f9e91a7a57d88551588324c933e76b8336313a38c24747964
7
- data.tar.gz: 6573bfbd3c6ff73b83b957b9b6e784486079b194c9d09c1c4803b16f0336b51376d39ddab369ffdfe5d13ffaa129ecb727256c27f4683169250f7c01fa9498bd
6
+ metadata.gz: 6b501e8559461f9a2a24aa9515403f6ee3d071786005f56eb1e466560b5b53a78f4992fc8c4b8ae9c17a819263c401b7474b732a21b261bb7890902e88a69f05
7
+ data.tar.gz: 0ee901ddfa558db983229086f6f5e4cd20de2c9575c20a6457e09862b5e2d1eeadd662e856de1851e6528b834a0a5dcfe714f01a8b521d542a2927e14aac4552
@@ -1,3 +1,27 @@
1
+ # v2.5.3
2
+ Use a constant time compare algorithm for checking a signature
3
+
4
+ # v2.5.2
5
+ Adjust `Cerner::OAuth1a::Protocol.parse_www_authenticate_header` to handle parameters
6
+ that are either tokens or quoted strings.
7
+
8
+ # v2.5.1
9
+ Address `instance variable @cache_instance not initialized` warning
10
+
11
+ # v2.5.0
12
+ Add Consumer and Provider support for HMAC-SHA1 signatures.
13
+
14
+ Added a Cerner::OAuth1a::Protocol.percent_encode method.
15
+
16
+ Correctly percent encodes PLAINTEXT signature parts (client shared secret and token
17
+ shared secret) before constructing PLAINTEXT signature.
18
+
19
+ # v2.4.0
20
+ Handle nonce and timestamp as optional fields Per
21
+ https://tools.ietf.org/html/rfc5849#section-3.1, the oauth_timestamp and oauth_nonce
22
+ fields may be omitted when PLAINTEXT signatures are used. This commit make the APIs
23
+ related to those two fields treat the data as optional.
24
+
1
25
  # v2.3.0
2
26
  Added Protection Realm Equivalence feature to Cerner::OAuth1a::AccessTokenAgent,
3
27
  which is used by Cerner::OAuth1a::AccessToken#authenticate when comparing realms.
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Build Status](https://api.travis-ci.com/cerner/cerner-oauth1a.svg)](https://travis-ci.com/cerner/cerner-oauth1a)
4
4
  [![Gem Version](http://img.shields.io/gem/v/cerner-oauth1a.svg)](https://rubygems.org/gems/cerner-oauth1a)
5
- [![Code Climate](http://img.shields.io/codeclimate/github/cerner/cerner-oauth1a.svg)](https://codeclimate.com/github/cerner/cerner-oauth1a)
5
+ [![AwesomeCode Status](https://awesomecode.io/projects/48ece237-ac9c-49c9-859a-3a825968339b/status)](https://awesomecode.io/repos/cerner/cerner-oauth1a)
6
6
 
7
7
  A minimal dependency library for interacting with a Cerner OAuth 1.0a Access Token Service for
8
8
  invoking Cerner OAuth 1.0a protected services or implementing Cerner OAuth 1.0a authentication.
@@ -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,12 +1,13 @@
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
8
10
  module OAuth1a
9
-
10
11
  # Public: A Cerner OAuth 1.0a Access Token and related request parameters for use in Consumer or
11
12
  # Service Provider use cases.
12
13
  class AccessToken
@@ -29,10 +30,6 @@ module Cerner
29
30
  missing_params = []
30
31
  consumer_key = params[:oauth_consumer_key]
31
32
  missing_params << :oauth_consumer_key if consumer_key.nil? || consumer_key.empty?
32
- nonce = params[:oauth_nonce]
33
- missing_params << :oauth_nonce if nonce.nil? || nonce.empty?
34
- timestamp = params[:oauth_timestamp]
35
- missing_params << :oauth_timestamp if timestamp.nil? || timestamp.empty?
36
33
  token = params[:oauth_token]
37
34
  missing_params << :oauth_token if token.nil? || token.empty?
38
35
  signature_method = params[:oauth_signature_method]
@@ -43,9 +40,10 @@ module Cerner
43
40
  raise OAuthError.new('', nil, 'parameter_absent', missing_params) unless missing_params.empty?
44
41
 
45
42
  AccessToken.new(
43
+ accessor_secret: params[:oauth_accessor_secret],
46
44
  consumer_key: consumer_key,
47
- nonce: nonce,
48
- timestamp: timestamp,
45
+ nonce: params[:oauth_nonce],
46
+ timestamp: params[:oauth_timestamp],
49
47
  token: token,
50
48
  signature_method: signature_method,
51
49
  signature: signature,
@@ -53,15 +51,18 @@ module Cerner
53
51
  )
54
52
  end
55
53
 
56
- # 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.
57
56
  attr_reader :accessor_secret
58
57
  # Returns a String with the Consumer Key (oauth_consumer_key) related to this token.
59
58
  attr_reader :consumer_key
60
59
  # Returns a Time, but may be nil, which represents the moment when this token expires.
61
60
  attr_reader :expires_at
62
- # Returns a String 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.
63
63
  attr_reader :nonce
64
- # Returns a Time, 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.
65
66
  attr_reader :timestamp
66
67
  # Returns a String with the Token (oauth_token).
67
68
  attr_reader :token
@@ -86,85 +87,150 @@ module Cerner
86
87
  # :expires_at - An optional Time representing the expiration moment or any
87
88
  # object responding to to_i that represents the expiration
88
89
  # moment as the number of seconds since the epoch.
89
- # :nonce - The required String representing the nonce.
90
- # :timestamp - A required Time representing the creation moment or any
90
+ # :nonce - The optional String representing the nonce.
91
+ # :timestamp - A optional Time representing the creation moment or any
91
92
  # object responding to to_i that represents the creation
92
93
  # moment as the number of seconds since the epoch.
93
94
  # :token - The required String representing the token.
94
- # :token_secret - The required String representing the token secret.
95
+ # :token_secret - The optional String representing the token secret.
95
96
  # :signature_method - The optional String representing the signature method.
96
97
  # Defaults to PLAINTEXT.
97
98
  # :signature - The optional String representing the signature.
98
- # Defaults to nil.
99
99
  # :realm - The optional String representing the protection realm.
100
- # Defaults to nil.
101
100
  #
102
- # Raises ArgumentError if consumer_key, nonce, timestamp, token or signature_method is nil.
101
+ # Raises ArgumentError if consumer_key or token is nil.
103
102
  def initialize(
104
103
  accessor_secret: nil,
105
104
  consumer_key:,
106
105
  expires_at: nil,
107
- nonce:,
106
+ nonce: nil,
108
107
  signature: nil,
109
108
  signature_method: 'PLAINTEXT',
110
- timestamp:,
109
+ timestamp: nil,
111
110
  token:,
112
111
  token_secret: nil,
113
112
  realm: nil
114
113
  )
115
114
  raise ArgumentError, 'consumer_key is nil' unless consumer_key
116
- raise ArgumentError, 'nonce is nil' unless nonce
117
- raise ArgumentError, 'timestamp is nil' unless timestamp
118
115
  raise ArgumentError, 'token is nil' unless token
119
116
 
120
117
  @accessor_secret = accessor_secret || nil
121
- @authorization_header = nil
122
118
  @consumer_key = consumer_key
123
119
  @consumer_principal = nil
124
- @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
125
121
  @nonce = nonce
126
122
  @signature = signature
127
123
  @signature_method = signature_method || 'PLAINTEXT'
128
- @timestamp = convert_to_time(timestamp)
124
+ @timestamp = timestamp ? Internal.convert_to_time(time: timestamp, name: 'timestamp') : nil
129
125
  @token = token
130
126
  @token_secret = token_secret || nil
131
127
  @realm = realm || nil
132
128
  end
133
129
 
134
130
  # Public: Generates a value suitable for use as an HTTP Authorization header. If #signature is
135
- # nil, then #accessor_secret and #token_secret will be used to build a signature via the
136
- # 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.
137
171
  #
138
172
  # Returns a String representation of the access token.
139
173
  #
140
174
  # Raises Cerner::OAuth1a::OAuthError if #signature_method is not PLAINTEXT or if a signature
141
175
  # can't be determined.
142
- def authorization_header
143
- return @authorization_header if @authorization_header
144
-
145
- unless @signature_method == 'PLAINTEXT'
146
- raise OAuthError.new('signature_method must be PLAINTEXT', nil, 'signature_method_rejected', nil, @realm)
147
- 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
148
186
 
149
187
  if @signature
150
188
  sig = @signature
151
- elsif @accessor_secret && @token_secret
152
- sig = "#{@accessor_secret}&#{@token_secret}"
153
189
  else
154
- 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
155
228
  end
156
229
 
157
- tuples = {
158
- realm: @realm,
159
- oauth_version: '1.0',
160
- oauth_signature_method: @signature_method,
161
- oauth_signature: sig,
162
- oauth_consumer_key: @consumer_key,
163
- oauth_nonce: @nonce,
164
- oauth_timestamp: @timestamp.tv_sec,
165
- oauth_token: @token
166
- }
167
- @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)
168
234
  end
169
235
 
170
236
  # Public: Authenticates the #token against the #consumer_key, #signature and side-channel
@@ -175,13 +241,27 @@ module Cerner
175
241
  # access_token_agent - An instance of Cerner::OAuth1a::AccessTokenAgent configured with
176
242
  # appropriate credentials to retrieve secrets via
177
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.
178
253
  #
179
254
  # Returns a Hash (symbolized keys) of any extra parameters within #token (oauth_token),
180
255
  # if authentication succeeds. In most scenarios, the Hash will be empty.
181
256
  #
182
257
  # Raises ArgumentError if access_token_agent is nil
183
258
  # Raises Cerner::OAuth1a::OAuthError with an oauth_problem if authentication fails.
184
- 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
+ )
185
265
  raise ArgumentError, 'access_token_agent is nil' unless access_token_agent
186
266
 
187
267
  if @realm && !access_token_agent.realm_eql?(@realm)
@@ -191,10 +271,6 @@ module Cerner
191
271
  # Set realm to the provider's realm if it's not already set
192
272
  @realm ||= access_token_agent.realm
193
273
 
194
- unless @signature_method == 'PLAINTEXT'
195
- raise OAuthError.new('signature_method must be PLAINTEXT', nil, 'signature_method_rejected', nil, @realm)
196
- end
197
-
198
274
  tuples = Protocol.parse_url_query_string(@token)
199
275
 
200
276
  unless @consumer_key == tuples.delete(:ConsumerKey)
@@ -209,7 +285,13 @@ module Cerner
209
285
  # RSASHA1 param gets consumed in #verify_token, so remove it too
210
286
  tuples.delete(:RSASHA1)
211
287
 
212
- 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
+ )
213
295
 
214
296
  @consumer_principal = tuples.delete(:"Consumer.Principal")
215
297
 
@@ -231,7 +313,7 @@ module Cerner
231
313
  # if @expires_at is nil, return true now
232
314
  return true unless @expires_at
233
315
 
234
- now = convert_to_time(now)
316
+ now = Internal.convert_to_time(time: now, name: 'now')
235
317
  now.tv_sec >= @expires_at.tv_sec - fudge_sec
236
318
  end
237
319
 
@@ -283,59 +365,22 @@ module Cerner
283
365
 
284
366
  private
285
367
 
286
- # Internal: Used by #initialize and #expired? to convert data into a Time instance.
287
- #
288
- # time - Time or any object with a #to_i the returns an Integer.
289
- #
290
- # Returns a Time instance in the UTC time zone.
291
- def convert_to_time(time)
292
- raise ArgumentError, 'time is nil' unless time
293
-
294
- if time.is_a? Time
295
- time.utc
296
- else
297
- Time.at(time.to_i).utc
298
- end
299
- end
300
-
301
368
  # Internal: Used by #authenticate to verify the expiration time.
302
- #
303
- # expires_on - The ExpiresOn parameter of oauth_token
304
- #
305
- # Raises OAuthError if the parameter is invalid or expired
306
369
  def verify_expiration(expires_on)
307
370
  unless expires_on
308
- raise OAuthError.new(
309
- 'token missing ExpiresOn',
310
- nil,
311
- 'oauth_parameters_rejected',
312
- 'oauth_token',
313
- @realm
314
- )
371
+ raise OAuthError.new('token missing ExpiresOn', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
315
372
  end
316
373
 
317
- expires_on = convert_to_time(expires_on)
318
- now = convert_to_time(Time.now)
319
- if now.tv_sec >= expires_on.tv_sec
320
- raise OAuthError.new(
321
- 'token has expired',
322
- nil,
323
- 'token_expired',
324
- nil,
325
- @realm
326
- )
327
- end
374
+ expires_on = Internal.convert_to_time(time: expires_on, name: 'expires_on')
375
+ now = Internal.convert_to_time(time: Time.now)
376
+
377
+ raise OAuthError.new('token has expired', nil, 'token_expired', nil, @realm) if now.tv_sec >= expires_on.tv_sec
328
378
  end
329
379
 
380
+ # Internal: Used by #authenticate to load the keys
330
381
  def load_keys(access_token_agent, keys_version)
331
382
  unless keys_version
332
- raise OAuthError.new(
333
- 'token missing KeysVersion',
334
- nil,
335
- 'oauth_parameters_rejected',
336
- 'oauth_token',
337
- @realm
338
- )
383
+ raise OAuthError.new('token missing KeysVersion', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
339
384
  end
340
385
 
341
386
  begin
@@ -352,24 +397,14 @@ module Cerner
352
397
  end
353
398
 
354
399
  # Internal: Used by #authenticate to verify the oauth_token value.
355
- #
356
- # keys - The Keys instance that contains the key used to sign the oauth_token
357
- #
358
- # Raises OAuthError if the parameter is not authentic
359
400
  def verify_token(keys)
360
- unless keys.verify_rsasha1_signature(@token)
361
- raise OAuthError.new('token is not authentic', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
362
- 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)
363
404
  end
364
405
 
365
406
  # Internal: Used by #authenticate to verify the request signature.
366
- #
367
- # keys - The Keys instance that contains the key used to encrypt the HMACSecrets
368
- # hmac_secrets - The HMACSecrets parameter of oauth_token
369
- #
370
- # Raises OAuthError if there is no signature, the parameter is invalid or the signature does
371
- # not match the secrets
372
- def verify_signature(keys, hmac_secrets)
407
+ def verify_signature(keys:, hmac_secrets:, http_method:, fully_qualified_url:, request_params:)
373
408
  unless @signature
374
409
  raise OAuthError.new('missing signature', nil, 'oauth_parameters_absent', 'oauth_signature', @realm)
375
410
  end
@@ -390,11 +425,58 @@ module Cerner
390
425
  end
391
426
 
392
427
  secrets_parts = Protocol.parse_url_query_string(secrets)
393
- expected_signature = "#{secrets_parts[:ConsumerSecret]}&#{secrets_parts[:TokenSecret]}"
394
428
 
395
- unless @signature == expected_signature
396
- 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
+ )
397
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)
398
480
  end
399
481
  end
400
482
  end