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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +62 -0
- data/lib/cerner/oauth1a.rb +1 -0
- data/lib/cerner/oauth1a/access_token.rb +185 -81
- data/lib/cerner/oauth1a/access_token_agent.rb +66 -63
- data/lib/cerner/oauth1a/cache.rb +12 -4
- data/lib/cerner/oauth1a/cache_rails.rb +1 -7
- data/lib/cerner/oauth1a/internal.rb +66 -0
- data/lib/cerner/oauth1a/protocol.rb +16 -9
- data/lib/cerner/oauth1a/signature.rb +157 -0
- data/lib/cerner/oauth1a/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d923d6f574a554d2995de4c00c3a97924a3750f424f1b141accd4eb239401e70
|
4
|
+
data.tar.gz: 9f72e25bc46201b3a071b1b34e8450d40fd9db0e29ac02c1ab6153742357cdf9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d752ee31d28626e1bdf135e20347f240b9658025cfffdb46d4e2b409aaba7ec2222ba30263b0aeedce0491168af8ea65dfc267b45f1345e77c6773cbbf767c7b
|
7
|
+
data.tar.gz: 848a69c486be5a55861fce17cc8cc9da4102ddda4b9540c37eb19ada65b83628a28c8d9571bdf2ef7b7ed0d0e545b294da4ecd92aed891daeb9221172adf05de
|
data/CHANGELOG.md
CHANGED
@@ -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
|
data/lib/cerner/oauth1a.rb
CHANGED
@@ -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
|
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,
|
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
|
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
|
127
|
-
#
|
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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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(
|
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(
|
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
|
-
|
339
|
-
|
340
|
-
|
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
|
-
|
374
|
-
|
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/
|
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,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
|
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 =
|
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
|
88
|
+
return unless params && !params.empty?
|
77
89
|
|
78
90
|
realm = "realm=\"#{params.delete(:realm)}\"" if params[:realm]
|
79
|
-
realm += ',
|
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
|
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
|
+
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-
|
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:
|