cerner-oauth1a 2.3.0 → 2.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/NOTICE +1 -1
- data/README.md +64 -2
- data/lib/cerner/oauth1a.rb +1 -0
- data/lib/cerner/oauth1a/access_token.rb +195 -113
- data/lib/cerner/oauth1a/access_token_agent.rb +66 -63
- data/lib/cerner/oauth1a/cache.rb +13 -5
- data/lib/cerner/oauth1a/cache_rails.rb +3 -9
- data/lib/cerner/oauth1a/internal.rb +95 -0
- data/lib/cerner/oauth1a/protocol.rb +49 -16
- data/lib/cerner/oauth1a/signature.rb +157 -0
- data/lib/cerner/oauth1a/version.rb +1 -1
- metadata +8 -6
@@ -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/
|
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
|
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
|
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
|
168
|
+
request = retrieve_prepare_request(timestamp: timestamp, accessor_secret: accessor_secret, principal: principal)
|
161
169
|
response = http_client.request(request)
|
162
|
-
access_token =
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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
|
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 =
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
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
|
-
|
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
|
data/lib/cerner/oauth1a/cache.rb
CHANGED
@@ -6,17 +6,17 @@ 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
|
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
|
-
return @cache_instance if @cache_instance
|
19
|
+
return @cache_instance if instance_variable_defined?(:@cache_instance) && @cache_instance
|
20
20
|
|
21
21
|
@cache_instance = DefaultCache.new(max: 50)
|
22
22
|
end
|
@@ -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 =
|
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)
|
@@ -2,12 +2,11 @@
|
|
2
2
|
|
3
3
|
module Cerner
|
4
4
|
module OAuth1a
|
5
|
-
|
6
5
|
# Internal: A Railtie that initializer the cache implementation to use Rails.cache.
|
7
6
|
# This will be picked up automatically if ::Rails and ::Rails.cache are defined.
|
8
7
|
class CacheRailtie < ::Rails::Railtie
|
9
8
|
initializer 'cerner-oauth1a.cache_initialization' do |_app|
|
10
|
-
::Rails.logger.info
|
9
|
+
::Rails.logger.info("#{CacheRailtie.name}: configuring cache to use Rails.cache")
|
11
10
|
Cerner::OAuth1a::Cache.instance = RailsCache.new(::Rails.cache)
|
12
11
|
end
|
13
12
|
end
|
@@ -20,6 +19,7 @@ module Cerner
|
|
20
19
|
#
|
21
20
|
# rails_cache - An instance of ActiveSupport::Cache::Store.
|
22
21
|
def initialize(rails_cache)
|
22
|
+
super()
|
23
23
|
@cache = rails_cache
|
24
24
|
end
|
25
25
|
|
@@ -29,13 +29,7 @@ module Cerner
|
|
29
29
|
# key - The key for the cache entries, which is qualified by namespace.
|
30
30
|
# entry - The entry to be stored in the cache.
|
31
31
|
def put(namespace, key, entry)
|
32
|
-
@cache.write(
|
33
|
-
key,
|
34
|
-
entry,
|
35
|
-
namespace: namespace,
|
36
|
-
expires_in: entry.expires_in,
|
37
|
-
race_condition_ttl: 5
|
38
|
-
)
|
32
|
+
@cache.write(key, entry, namespace: namespace, expires_in: entry.expires_in, race_condition_ttl: 5)
|
39
33
|
end
|
40
34
|
|
41
35
|
# Internal: Retrieves the entry, if available, from the cache store.
|
@@ -0,0 +1,95 @@
|
|
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
|
+
|
65
|
+
# Internal: Compares two Strings using a constant time algorithm to protect against timing
|
66
|
+
# attacks.
|
67
|
+
#
|
68
|
+
# left - The left String
|
69
|
+
# right - The right String
|
70
|
+
#
|
71
|
+
# Return true if left and right match, false otherwise.
|
72
|
+
def self.constant_time_compare(left, right)
|
73
|
+
max_size = [left.bytesize, right.bytesize].max
|
74
|
+
# convert left and right to array of bytes (Integer)
|
75
|
+
left = left.unpack('C*')
|
76
|
+
right = right.unpack('C*')
|
77
|
+
|
78
|
+
# if either array is not the max size, expand it with zeros
|
79
|
+
# having equal arrays keeps the algorithm execution time constant
|
80
|
+
left = left.fill(0, left.size, max_size - left.size) if left.size < max_size
|
81
|
+
right = right.fill(0, right.size, max_size - right.size) if right.size < max_size
|
82
|
+
|
83
|
+
result = 0
|
84
|
+
left.each_with_index do |left_value, i|
|
85
|
+
# XOR the two bytes, if equal, the operation is 0
|
86
|
+
# OR the XOR operation with the previous result
|
87
|
+
result |= left_value ^ right[i]
|
88
|
+
end
|
89
|
+
|
90
|
+
# if every comparison resuled in 0, then left and right are equal
|
91
|
+
result.zero?
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|