cerner-oauth1a 2.4.0 → 2.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/NOTICE +1 -1
- data/README.md +63 -1
- data/lib/cerner/oauth1a.rb +1 -0
- data/lib/cerner/oauth1a/access_token.rb +190 -86
- 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 +2 -7
- data/lib/cerner/oauth1a/internal.rb +95 -0
- data/lib/cerner/oauth1a/protocol.rb +32 -13
- data/lib/cerner/oauth1a/signature.rb +157 -0
- data/lib/cerner/oauth1a/version.rb +1 -1
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7c216d39c6c458e3987b9b46dbedf8cd58c462fd336d1042d1f7f827e0be71f3
|
4
|
+
data.tar.gz: 79166e522691bbac84d2190bad3f824441eb3b2c8448c6547c1874a627703f4e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 298d94d966ff6bed04ab29f8ee8ad3076507e9cc950517eff5bf0fb9c31d15b89924fa5c1fe384b1fd20f16688327b90267178b7bcf80b9958dcd6e0cb2d0b2d
|
7
|
+
data.tar.gz: d3e0c95062ed21513a20c47c2115f0385260173f60ed9c7676c320084b787c2488d301f7436330ebdae9f78d044790837c2694197e34ef70f2ba3d769bb61cad
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,26 @@
|
|
1
|
+
# v2.5.4
|
2
|
+
Replace invalid usage of `oauth_parameters_rejected` and `oauth_parameters_absent`
|
3
|
+
OAuth problem values with correct values `parameter_rejected` and `parameter_absent`
|
4
|
+
when reporting certain errors.
|
5
|
+
|
6
|
+
# v2.5.3
|
7
|
+
Use a constant time compare algorithm for checking a signature
|
8
|
+
|
9
|
+
# v2.5.2
|
10
|
+
Adjust `Cerner::OAuth1a::Protocol.parse_www_authenticate_header` to handle parameters
|
11
|
+
that are either tokens or quoted strings.
|
12
|
+
|
13
|
+
# v2.5.1
|
14
|
+
Address `instance variable @cache_instance not initialized` warning
|
15
|
+
|
16
|
+
# v2.5.0
|
17
|
+
Add Consumer and Provider support for HMAC-SHA1 signatures.
|
18
|
+
|
19
|
+
Added a Cerner::OAuth1a::Protocol.percent_encode method.
|
20
|
+
|
21
|
+
Correctly percent encodes PLAINTEXT signature parts (client shared secret and token
|
22
|
+
shared secret) before constructing PLAINTEXT signature.
|
23
|
+
|
1
24
|
# v2.4.0
|
2
25
|
Handle nonce and timestamp as optional fields Per
|
3
26
|
https://tools.ietf.org/html/rfc5849#section-3.1, the oauth_timestamp and oauth_nonce
|
data/NOTICE
CHANGED
data/README.md
CHANGED
@@ -38,7 +38,53 @@ for implementing a Ruby-based service.
|
|
38
38
|
# Invoke the API's HTTP endpoint and use the AccessToken to generate an Authorization header
|
39
39
|
response = http.request_get(uri.path, Authorization: access_token.authorization_header)
|
40
40
|
|
41
|
+
### Consumer HMAC-SHA1 Signature Method
|
42
|
+
|
43
|
+
The preferred and default signature method is PLAINTEXT, as all communication SHOULD be via TLS. However, if HMAC-SHA1 signatures are necessary, then this can be achieved by constructing AccessTokenAgent as follows:
|
44
|
+
|
45
|
+
agent = Cerner::OAuth1a::AccessTokenAgent.new(
|
46
|
+
access_token_url: 'https://oauth-api.cerner.com/oauth/access',
|
47
|
+
consumer_key: 'CONSUMER_KEY',
|
48
|
+
consumer_secret: 'CONSUMER_SECRET',
|
49
|
+
signature_method: 'HMAC-SHA1'
|
50
|
+
)
|
51
|
+
|
52
|
+
To use the AccessToken requires additional parameters to be passed when constructing the Authorization header. The HTTP method, the URL being invoked and all request parameters. The request parameters should include all parameters passed in the query string and those passed in the body if the Content-Type of the body is `application/x-www-form-urlencoded`. See the specification for more details.
|
53
|
+
|
54
|
+
#### Consumer HMAC-SHA1 Signature Method Examples
|
55
|
+
|
56
|
+
GET with no request parameters
|
57
|
+
|
58
|
+
uri = URI('https://authz-demo-api.cerner.com/me')
|
59
|
+
# ...
|
60
|
+
authz_header = access_token.authorization_header(fully_qualified_url: uri)
|
61
|
+
|
62
|
+
GET with request parameters in URL
|
63
|
+
|
64
|
+
uri = URI('https://authz-demo-api.cerner.com/me?name=value')
|
65
|
+
# ...
|
66
|
+
authz_header = access_token.authorization_header(fully_qualified_url: uri)
|
67
|
+
|
68
|
+
POST with request parameters (form post)
|
69
|
+
|
70
|
+
authz_header = access_token.authorization_header(
|
71
|
+
http_method: 'POST'
|
72
|
+
fully_qualified_url: 'https://example/path',
|
73
|
+
request_params: {
|
74
|
+
sort: 'asc',
|
75
|
+
field: ['name', 'desc'] # sending the field multiple times
|
76
|
+
}
|
77
|
+
)
|
78
|
+
|
79
|
+
PUT with no request parameters (entity body)
|
80
|
+
|
81
|
+
authz_header = access_token.authorization_header(
|
82
|
+
http_method: 'PUT'
|
83
|
+
fully_qualified_url: 'https://example/path'
|
84
|
+
)
|
85
|
+
|
41
86
|
### Access Token Reuse
|
87
|
+
|
42
88
|
Generally, you'll want to use an Access Token more than once. Access Tokens can be reused, but
|
43
89
|
they do expire, so you'll need to acquire new tokens after one expires. All of the expiration
|
44
90
|
information is contained in the AccessToken class and you can easily determine if a token is
|
@@ -77,6 +123,21 @@ implement that:
|
|
77
123
|
# (xoauth_principal)
|
78
124
|
consumer_principal = access_token.consumer_principal
|
79
125
|
|
126
|
+
### Service Provider HMAC-SHA1 Signature Method
|
127
|
+
|
128
|
+
The preferred and default signature method is PLAINTEXT, as all communication SHOULD be via TLS. However, if HMAC-SHA1 signatures are necessary, then this can be achieved by passing additional informational to the `authenticate` method.
|
129
|
+
|
130
|
+
begin
|
131
|
+
results = access_token.authenticate(
|
132
|
+
agent,
|
133
|
+
http_method: request.method,
|
134
|
+
fully_qualified_url: request.original_url,
|
135
|
+
request_params: request.parameters
|
136
|
+
)
|
137
|
+
rescue OAuthError => e
|
138
|
+
# respond with a 401
|
139
|
+
end
|
140
|
+
|
80
141
|
## Caching
|
81
142
|
|
82
143
|
The AccessTokenAgent class provides built-in memory caching. AccessTokens and Keys are cached
|
@@ -90,6 +151,7 @@ cache to use an implementation that stores the AccessTokens and Keys within Rail
|
|
90
151
|
|
91
152
|
## References
|
92
153
|
* https://wiki.ucern.com/display/public/reference/Cerner%27s+OAuth+Specification
|
154
|
+
* https://tools.ietf.org/html/rfc5849
|
93
155
|
* http://oauth.net/core/1.0a
|
94
156
|
* http://oauth.pbwiki.com/ProblemReporting
|
95
157
|
* https://wiki.ucern.com/display/public/reference/Accessing+Cerner%27s+Web+Services+Using+OAuth+1.0a
|
@@ -149,7 +211,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
149
211
|
|
150
212
|
# LICENSE
|
151
213
|
|
152
|
-
Copyright
|
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
|
|
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,46 +365,22 @@ module Cerner
|
|
274
365
|
|
275
366
|
private
|
276
367
|
|
277
|
-
# Internal: Used by #initialize and #expired? to convert data into a Time instance.
|
278
|
-
#
|
279
|
-
# time - Time or any object with a #to_i the returns an Integer.
|
280
|
-
#
|
281
|
-
# Returns a Time instance in the UTC time zone.
|
282
|
-
def convert_to_time(time)
|
283
|
-
raise ArgumentError, 'time is nil' unless time
|
284
|
-
|
285
|
-
if time.is_a?(Time)
|
286
|
-
time.utc
|
287
|
-
else
|
288
|
-
Time.at(time.to_i).utc
|
289
|
-
end
|
290
|
-
end
|
291
|
-
|
292
368
|
# Internal: Used by #authenticate to verify the expiration time.
|
293
|
-
#
|
294
|
-
# expires_on - The ExpiresOn parameter of oauth_token
|
295
|
-
#
|
296
|
-
# Raises OAuthError if the parameter is invalid or expired
|
297
369
|
def verify_expiration(expires_on)
|
298
370
|
unless expires_on
|
299
|
-
raise OAuthError.new(
|
300
|
-
'token missing ExpiresOn',
|
301
|
-
nil,
|
302
|
-
'oauth_parameters_rejected',
|
303
|
-
'oauth_token',
|
304
|
-
@realm
|
305
|
-
)
|
371
|
+
raise OAuthError.new('token missing ExpiresOn', nil, 'parameter_rejected', 'oauth_token', @realm)
|
306
372
|
end
|
307
373
|
|
308
|
-
expires_on = convert_to_time(expires_on)
|
309
|
-
now = convert_to_time(Time.now)
|
374
|
+
expires_on = Internal.convert_to_time(time: expires_on, name: 'expires_on')
|
375
|
+
now = Internal.convert_to_time(time: Time.now)
|
310
376
|
|
311
377
|
raise OAuthError.new('token has expired', nil, 'token_expired', nil, @realm) if now.tv_sec >= expires_on.tv_sec
|
312
378
|
end
|
313
379
|
|
380
|
+
# Internal: Used by #authenticate to load the keys
|
314
381
|
def load_keys(access_token_agent, keys_version)
|
315
382
|
unless keys_version
|
316
|
-
raise OAuthError.new('token missing KeysVersion', nil, '
|
383
|
+
raise OAuthError.new('token missing KeysVersion', nil, 'parameter_rejected', 'oauth_token', @realm)
|
317
384
|
end
|
318
385
|
|
319
386
|
begin
|
@@ -322,7 +389,7 @@ module Cerner
|
|
322
389
|
raise OAuthError.new(
|
323
390
|
'token references invalid keys version',
|
324
391
|
nil,
|
325
|
-
'
|
392
|
+
'parameter_rejected',
|
326
393
|
'oauth_token',
|
327
394
|
@realm
|
328
395
|
)
|
@@ -330,29 +397,19 @@ module Cerner
|
|
330
397
|
end
|
331
398
|
|
332
399
|
# Internal: Used by #authenticate to verify the oauth_token value.
|
333
|
-
#
|
334
|
-
# keys - The Keys instance that contains the key used to sign the oauth_token
|
335
|
-
#
|
336
|
-
# Raises OAuthError if the parameter is not authentic
|
337
400
|
def verify_token(keys)
|
338
|
-
|
339
|
-
|
340
|
-
|
401
|
+
return if keys.verify_rsasha1_signature(@token)
|
402
|
+
|
403
|
+
raise OAuthError.new('token is not authentic', nil, 'parameter_rejected', 'oauth_token', @realm)
|
341
404
|
end
|
342
405
|
|
343
406
|
# Internal: Used by #authenticate to verify the request signature.
|
344
|
-
|
345
|
-
# keys - The Keys instance that contains the key used to encrypt the HMACSecrets
|
346
|
-
# hmac_secrets - The HMACSecrets parameter of oauth_token
|
347
|
-
#
|
348
|
-
# Raises OAuthError if there is no signature, the parameter is invalid or the signature does
|
349
|
-
# not match the secrets
|
350
|
-
def verify_signature(keys, hmac_secrets)
|
407
|
+
def verify_signature(keys:, hmac_secrets:, http_method:, fully_qualified_url:, request_params:)
|
351
408
|
unless @signature
|
352
|
-
raise OAuthError.new('missing signature', nil, '
|
409
|
+
raise OAuthError.new('missing signature', nil, 'parameter_absent', 'oauth_signature', @realm)
|
353
410
|
end
|
354
411
|
unless hmac_secrets
|
355
|
-
raise OAuthError.new('missing HMACSecrets', nil, '
|
412
|
+
raise OAuthError.new('missing HMACSecrets', nil, 'parameter_rejected', 'oauth_token', @realm)
|
356
413
|
end
|
357
414
|
|
358
415
|
begin
|
@@ -361,18 +418,65 @@ module Cerner
|
|
361
418
|
raise OAuthError.new(
|
362
419
|
"unable to decrypt HMACSecrets: #{e.message}",
|
363
420
|
nil,
|
364
|
-
'
|
421
|
+
'parameter_rejected',
|
365
422
|
'oauth_token',
|
366
423
|
@realm
|
367
424
|
)
|
368
425
|
end
|
369
426
|
|
370
427
|
secrets_parts = Protocol.parse_url_query_string(secrets)
|
371
|
-
expected_signature = "#{secrets_parts[:ConsumerSecret]}&#{secrets_parts[:TokenSecret]}"
|
372
428
|
|
373
|
-
|
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 Internal.constant_time_compare(@signature, expected_signature)
|
478
|
+
|
479
|
+
raise OAuthError.new('signature is not valid', nil, 'signature_invalid', nil, @realm)
|
376
480
|
end
|
377
481
|
end
|
378
482
|
end
|