cerner-oauth1a 2.3.0 → 2.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a554af1b8a4a44ff14a05c9fb71517046cdb1a5bf677b5a4792eaa7d01423b83
|
4
|
+
data.tar.gz: 3f16a0c5584bae17e6edfcecb277c92d04da581fcc883f687b91719089543b8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b501e8559461f9a2a24aa9515403f6ee3d071786005f56eb1e466560b5b53a78f4992fc8c4b8ae9c17a819263c401b7474b732a21b261bb7890902e88a69f05
|
7
|
+
data.tar.gz: 0ee901ddfa558db983229086f6f5e4cd20de2c9575c20a6457e09862b5e2d1eeadd662e856de1851e6528b834a0a5dcfe714f01a8b521d542a2927e14aac4552
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,27 @@
|
|
1
|
+
# v2.5.3
|
2
|
+
Use a constant time compare algorithm for checking a signature
|
3
|
+
|
4
|
+
# v2.5.2
|
5
|
+
Adjust `Cerner::OAuth1a::Protocol.parse_www_authenticate_header` to handle parameters
|
6
|
+
that are either tokens or quoted strings.
|
7
|
+
|
8
|
+
# v2.5.1
|
9
|
+
Address `instance variable @cache_instance not initialized` warning
|
10
|
+
|
11
|
+
# v2.5.0
|
12
|
+
Add Consumer and Provider support for HMAC-SHA1 signatures.
|
13
|
+
|
14
|
+
Added a Cerner::OAuth1a::Protocol.percent_encode method.
|
15
|
+
|
16
|
+
Correctly percent encodes PLAINTEXT signature parts (client shared secret and token
|
17
|
+
shared secret) before constructing PLAINTEXT signature.
|
18
|
+
|
19
|
+
# v2.4.0
|
20
|
+
Handle nonce and timestamp as optional fields Per
|
21
|
+
https://tools.ietf.org/html/rfc5849#section-3.1, the oauth_timestamp and oauth_nonce
|
22
|
+
fields may be omitted when PLAINTEXT signatures are used. This commit make the APIs
|
23
|
+
related to those two fields treat the data as optional.
|
24
|
+
|
1
25
|
# v2.3.0
|
2
26
|
Added Protection Realm Equivalence feature to Cerner::OAuth1a::AccessTokenAgent,
|
3
27
|
which is used by Cerner::OAuth1a::AccessToken#authenticate when comparing realms.
|
data/NOTICE
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[![Build Status](https://api.travis-ci.com/cerner/cerner-oauth1a.svg)](https://travis-ci.com/cerner/cerner-oauth1a)
|
4
4
|
[![Gem Version](http://img.shields.io/gem/v/cerner-oauth1a.svg)](https://rubygems.org/gems/cerner-oauth1a)
|
5
|
-
[![
|
5
|
+
[![AwesomeCode Status](https://awesomecode.io/projects/48ece237-ac9c-49c9-859a-3a825968339b/status)](https://awesomecode.io/repos/cerner/cerner-oauth1a)
|
6
6
|
|
7
7
|
A minimal dependency library for interacting with a Cerner OAuth 1.0a Access Token Service for
|
8
8
|
invoking Cerner OAuth 1.0a protected services or implementing Cerner OAuth 1.0a authentication.
|
@@ -38,7 +38,53 @@ for implementing a Ruby-based service.
|
|
38
38
|
# Invoke the API's HTTP endpoint and use the AccessToken to generate an Authorization header
|
39
39
|
response = http.request_get(uri.path, Authorization: access_token.authorization_header)
|
40
40
|
|
41
|
+
### Consumer HMAC-SHA1 Signature Method
|
42
|
+
|
43
|
+
The preferred and default signature method is PLAINTEXT, as all communication SHOULD be via TLS. However, if HMAC-SHA1 signatures are necessary, then this can be achieved by constructing AccessTokenAgent as follows:
|
44
|
+
|
45
|
+
agent = Cerner::OAuth1a::AccessTokenAgent.new(
|
46
|
+
access_token_url: 'https://oauth-api.cerner.com/oauth/access',
|
47
|
+
consumer_key: 'CONSUMER_KEY',
|
48
|
+
consumer_secret: 'CONSUMER_SECRET',
|
49
|
+
signature_method: 'HMAC-SHA1'
|
50
|
+
)
|
51
|
+
|
52
|
+
To use the AccessToken requires additional parameters to be passed when constructing the Authorization header. The HTTP method, the URL being invoked and all request parameters. The request parameters should include all parameters passed in the query string and those passed in the body if the Content-Type of the body is `application/x-www-form-urlencoded`. See the specification for more details.
|
53
|
+
|
54
|
+
#### Consumer HMAC-SHA1 Signature Method Examples
|
55
|
+
|
56
|
+
GET with no request parameters
|
57
|
+
|
58
|
+
uri = URI('https://authz-demo-api.cerner.com/me')
|
59
|
+
# ...
|
60
|
+
authz_header = access_token.authorization_header(fully_qualified_url: uri)
|
61
|
+
|
62
|
+
GET with request parameters in URL
|
63
|
+
|
64
|
+
uri = URI('https://authz-demo-api.cerner.com/me?name=value')
|
65
|
+
# ...
|
66
|
+
authz_header = access_token.authorization_header(fully_qualified_url: uri)
|
67
|
+
|
68
|
+
POST with request parameters (form post)
|
69
|
+
|
70
|
+
authz_header = access_token.authorization_header(
|
71
|
+
http_method: 'POST'
|
72
|
+
fully_qualified_url: 'https://example/path',
|
73
|
+
request_params: {
|
74
|
+
sort: 'asc',
|
75
|
+
field: ['name', 'desc'] # sending the field multiple times
|
76
|
+
}
|
77
|
+
)
|
78
|
+
|
79
|
+
PUT with no request parameters (entity body)
|
80
|
+
|
81
|
+
authz_header = access_token.authorization_header(
|
82
|
+
http_method: 'PUT'
|
83
|
+
fully_qualified_url: 'https://example/path'
|
84
|
+
)
|
85
|
+
|
41
86
|
### Access Token Reuse
|
87
|
+
|
42
88
|
Generally, you'll want to use an Access Token more than once. Access Tokens can be reused, but
|
43
89
|
they do expire, so you'll need to acquire new tokens after one expires. All of the expiration
|
44
90
|
information is contained in the AccessToken class and you can easily determine if a token is
|
@@ -77,6 +123,21 @@ implement that:
|
|
77
123
|
# (xoauth_principal)
|
78
124
|
consumer_principal = access_token.consumer_principal
|
79
125
|
|
126
|
+
### Service Provider HMAC-SHA1 Signature Method
|
127
|
+
|
128
|
+
The preferred and default signature method is PLAINTEXT, as all communication SHOULD be via TLS. However, if HMAC-SHA1 signatures are necessary, then this can be achieved by passing additional informational to the `authenticate` method.
|
129
|
+
|
130
|
+
begin
|
131
|
+
results = access_token.authenticate(
|
132
|
+
agent,
|
133
|
+
http_method: request.method,
|
134
|
+
fully_qualified_url: request.original_url,
|
135
|
+
request_params: request.parameters
|
136
|
+
)
|
137
|
+
rescue OAuthError => e
|
138
|
+
# respond with a 401
|
139
|
+
end
|
140
|
+
|
80
141
|
## Caching
|
81
142
|
|
82
143
|
The AccessTokenAgent class provides built-in memory caching. AccessTokens and Keys are cached
|
@@ -90,6 +151,7 @@ cache to use an implementation that stores the AccessTokens and Keys within Rail
|
|
90
151
|
|
91
152
|
## References
|
92
153
|
* https://wiki.ucern.com/display/public/reference/Cerner%27s+OAuth+Specification
|
154
|
+
* https://tools.ietf.org/html/rfc5849
|
93
155
|
* http://oauth.net/core/1.0a
|
94
156
|
* http://oauth.pbwiki.com/ProblemReporting
|
95
157
|
* https://wiki.ucern.com/display/public/reference/Accessing+Cerner%27s+Web+Services+Using+OAuth+1.0a
|
@@ -149,7 +211,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
149
211
|
|
150
212
|
# LICENSE
|
151
213
|
|
152
|
-
Copyright
|
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,12 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'cerner/oauth1a/internal'
|
3
4
|
require 'cerner/oauth1a/oauth_error'
|
4
5
|
require 'cerner/oauth1a/protocol'
|
6
|
+
require 'cerner/oauth1a/signature'
|
5
7
|
require 'uri'
|
6
8
|
|
7
9
|
module Cerner
|
8
10
|
module OAuth1a
|
9
|
-
|
10
11
|
# Public: A Cerner OAuth 1.0a Access Token and related request parameters for use in Consumer or
|
11
12
|
# Service Provider use cases.
|
12
13
|
class AccessToken
|
@@ -29,10 +30,6 @@ module Cerner
|
|
29
30
|
missing_params = []
|
30
31
|
consumer_key = params[:oauth_consumer_key]
|
31
32
|
missing_params << :oauth_consumer_key if consumer_key.nil? || consumer_key.empty?
|
32
|
-
nonce = params[:oauth_nonce]
|
33
|
-
missing_params << :oauth_nonce if nonce.nil? || nonce.empty?
|
34
|
-
timestamp = params[:oauth_timestamp]
|
35
|
-
missing_params << :oauth_timestamp if timestamp.nil? || timestamp.empty?
|
36
33
|
token = params[:oauth_token]
|
37
34
|
missing_params << :oauth_token if token.nil? || token.empty?
|
38
35
|
signature_method = params[:oauth_signature_method]
|
@@ -43,9 +40,10 @@ module Cerner
|
|
43
40
|
raise OAuthError.new('', nil, 'parameter_absent', missing_params) unless missing_params.empty?
|
44
41
|
|
45
42
|
AccessToken.new(
|
43
|
+
accessor_secret: params[:oauth_accessor_secret],
|
46
44
|
consumer_key: consumer_key,
|
47
|
-
nonce:
|
48
|
-
timestamp:
|
45
|
+
nonce: params[:oauth_nonce],
|
46
|
+
timestamp: params[:oauth_timestamp],
|
49
47
|
token: token,
|
50
48
|
signature_method: signature_method,
|
51
49
|
signature: signature,
|
@@ -53,15 +51,18 @@ module Cerner
|
|
53
51
|
)
|
54
52
|
end
|
55
53
|
|
56
|
-
# Returns a String, but may be nil, with the Accessor Secret related
|
54
|
+
# Returns a String, but may be nil, with the Accessor Secret (oauth_accessor_secret) related
|
55
|
+
# to this token. Note: nil and empty are considered equivalent.
|
57
56
|
attr_reader :accessor_secret
|
58
57
|
# Returns a String with the Consumer Key (oauth_consumer_key) related to this token.
|
59
58
|
attr_reader :consumer_key
|
60
59
|
# Returns a Time, but may be nil, which represents the moment when this token expires.
|
61
60
|
attr_reader :expires_at
|
62
|
-
# Returns a String with the Nonce (oauth_nonce) related to this token.
|
61
|
+
# Returns a String, but may be nil, with the Nonce (oauth_nonce) related to this token. This
|
62
|
+
# is generally only populated when parsing a token for authentication.
|
63
63
|
attr_reader :nonce
|
64
|
-
# Returns a Time,
|
64
|
+
# Returns a Time, but may be nil, with the Timestamp (oauth_timestamp) related to this token.
|
65
|
+
# This is generally only populated when parsing a token for authentication.
|
65
66
|
attr_reader :timestamp
|
66
67
|
# Returns a String with the Token (oauth_token).
|
67
68
|
attr_reader :token
|
@@ -86,85 +87,150 @@ module Cerner
|
|
86
87
|
# :expires_at - An optional Time representing the expiration moment or any
|
87
88
|
# object responding to to_i that represents the expiration
|
88
89
|
# moment as the number of seconds since the epoch.
|
89
|
-
# :nonce - The
|
90
|
-
# :timestamp - A
|
90
|
+
# :nonce - The optional String representing the nonce.
|
91
|
+
# :timestamp - A optional Time representing the creation moment or any
|
91
92
|
# object responding to to_i that represents the creation
|
92
93
|
# moment as the number of seconds since the epoch.
|
93
94
|
# :token - The required String representing the token.
|
94
|
-
# :token_secret - The
|
95
|
+
# :token_secret - The optional String representing the token secret.
|
95
96
|
# :signature_method - The optional String representing the signature method.
|
96
97
|
# Defaults to PLAINTEXT.
|
97
98
|
# :signature - The optional String representing the signature.
|
98
|
-
# Defaults to nil.
|
99
99
|
# :realm - The optional String representing the protection realm.
|
100
|
-
# Defaults to nil.
|
101
100
|
#
|
102
|
-
# Raises ArgumentError if consumer_key
|
101
|
+
# Raises ArgumentError if consumer_key or token is nil.
|
103
102
|
def initialize(
|
104
103
|
accessor_secret: nil,
|
105
104
|
consumer_key:,
|
106
105
|
expires_at: nil,
|
107
|
-
nonce
|
106
|
+
nonce: nil,
|
108
107
|
signature: nil,
|
109
108
|
signature_method: 'PLAINTEXT',
|
110
|
-
timestamp
|
109
|
+
timestamp: nil,
|
111
110
|
token:,
|
112
111
|
token_secret: nil,
|
113
112
|
realm: nil
|
114
113
|
)
|
115
114
|
raise ArgumentError, 'consumer_key is nil' unless consumer_key
|
116
|
-
raise ArgumentError, 'nonce is nil' unless nonce
|
117
|
-
raise ArgumentError, 'timestamp is nil' unless timestamp
|
118
115
|
raise ArgumentError, 'token is nil' unless token
|
119
116
|
|
120
117
|
@accessor_secret = accessor_secret || nil
|
121
|
-
@authorization_header = nil
|
122
118
|
@consumer_key = consumer_key
|
123
119
|
@consumer_principal = nil
|
124
|
-
@expires_at = expires_at ? convert_to_time(expires_at) : nil
|
120
|
+
@expires_at = expires_at ? Internal.convert_to_time(time: expires_at, name: 'expires_at') : nil
|
125
121
|
@nonce = nonce
|
126
122
|
@signature = signature
|
127
123
|
@signature_method = signature_method || 'PLAINTEXT'
|
128
|
-
@timestamp = convert_to_time(timestamp)
|
124
|
+
@timestamp = timestamp ? Internal.convert_to_time(time: timestamp, name: 'timestamp') : nil
|
129
125
|
@token = token
|
130
126
|
@token_secret = token_secret || nil
|
131
127
|
@realm = realm || nil
|
132
128
|
end
|
133
129
|
|
134
130
|
# Public: Generates a value suitable for use as an HTTP Authorization header. If #signature is
|
135
|
-
# nil, then
|
136
|
-
#
|
131
|
+
# nil, then a signature will be generated based on the #signature_method.
|
132
|
+
#
|
133
|
+
# PLAINTEXT Signature (preferred)
|
134
|
+
#
|
135
|
+
# When using PLAINTEXT signatures, no additional arguments are necessary. If an oauth_nonce
|
136
|
+
# or oauth_timestamp are desired, then the values can be passed via the :nonce and :timestamp
|
137
|
+
# keyword arguments. The actual signature will be constructed from the Accessor Secret
|
138
|
+
# (#accessor_secret) and the Token Secret (#token_secret).
|
139
|
+
#
|
140
|
+
# HMAC-SHA1 Signature
|
141
|
+
#
|
142
|
+
# When using HMAC-SHA1 signatures, access to the HTTP request information is necessary. This
|
143
|
+
# requies that additional information is passed via the keyword arguments. The required
|
144
|
+
# information includes the HTTP method (see :http_method), the host authority & path (see
|
145
|
+
# :fully_qualified_url) and the request parameters (see :fully_qualified_url and
|
146
|
+
# :request_params).
|
147
|
+
#
|
148
|
+
# keywords - The keyword arguments:
|
149
|
+
# :nonce - The optional String containing a Nonce to generate the
|
150
|
+
# header with HMAC-SHA1 signatures. When nil, a Nonce will
|
151
|
+
# be generated.
|
152
|
+
# :timestamp - The optional Time or #to_i compliant object containing a
|
153
|
+
# Timestamp to generate the header with HMAC-SHA1
|
154
|
+
# signatures. When nil, a Timestamp will be generated.
|
155
|
+
# :http_method - The optional String or Symbol containing a HTTP Method for
|
156
|
+
# constructing the HMAC-SHA1 signature. When nil, the value
|
157
|
+
# defualts to 'GET'.
|
158
|
+
# :fully_qualified_url - The optional String or URI containing the fully qualified
|
159
|
+
# URL of the HTTP API being invoked for constructing the
|
160
|
+
# HMAC-SHA1 signature. If the URL contains a query string,
|
161
|
+
# the parameters will be extracted and used in addition to
|
162
|
+
# the :request_params keyword argument.
|
163
|
+
# :request_params - The optional Hash of name/value pairs containing the
|
164
|
+
# request parameters of the HTTP API being invoked for
|
165
|
+
# constructing the HMAC-SHA1 signature. Parameters passed
|
166
|
+
# here will override and augment those passed in the
|
167
|
+
# :fully_qualified_url parameter. The parameter names and
|
168
|
+
# values MUST be unencoded. See
|
169
|
+
# Protocol#parse_url_query_string for help with decoding an
|
170
|
+
# encoded query string.
|
137
171
|
#
|
138
172
|
# Returns a String representation of the access token.
|
139
173
|
#
|
140
174
|
# Raises Cerner::OAuth1a::OAuthError if #signature_method is not PLAINTEXT or if a signature
|
141
175
|
# can't be determined.
|
142
|
-
def authorization_header
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
176
|
+
def authorization_header(
|
177
|
+
nonce: nil, timestamp: nil, http_method: 'GET', fully_qualified_url: nil, request_params: nil
|
178
|
+
)
|
179
|
+
oauth_params = {}
|
180
|
+
oauth_params[:oauth_version] = '1.0'
|
181
|
+
oauth_params[:oauth_signature_method] = @signature_method
|
182
|
+
oauth_params[:oauth_consumer_key] = @consumer_key
|
183
|
+
oauth_params[:oauth_nonce] = nonce if nonce
|
184
|
+
oauth_params[:oauth_timestamp] = Internal.convert_to_time(time: timestamp, name: 'timestamp').to_i if timestamp
|
185
|
+
oauth_params[:oauth_token] = @token
|
148
186
|
|
149
187
|
if @signature
|
150
188
|
sig = @signature
|
151
|
-
elsif @accessor_secret && @token_secret
|
152
|
-
sig = "#{@accessor_secret}&#{@token_secret}"
|
153
189
|
else
|
154
|
-
|
190
|
+
# NOTE: @accessor_secret is always used, but an empty value is allowed and project assumes
|
191
|
+
# that nil implies an empty value
|
192
|
+
|
193
|
+
raise OAuthError.new('token_secret is nil', nil, 'parameter_absent', nil, @realm) unless @token_secret
|
194
|
+
|
195
|
+
if @signature_method == 'PLAINTEXT'
|
196
|
+
sig =
|
197
|
+
Signature.sign_via_plaintext(client_shared_secret: @accessor_secret, token_shared_secret: @token_secret)
|
198
|
+
elsif @signature_method == 'HMAC-SHA1'
|
199
|
+
http_method ||= 'GET' # default to HTTP GET
|
200
|
+
request_params ||= {} # default to no request params
|
201
|
+
oauth_params[:oauth_nonce] = Internal.generate_nonce unless oauth_params[:oauth_nonce]
|
202
|
+
oauth_params[:oauth_timestamp] = Internal.generate_timestamp unless oauth_params[:oauth_timestamp]
|
203
|
+
|
204
|
+
begin
|
205
|
+
fully_qualified_url = Internal.convert_to_http_uri(url: fully_qualified_url, name: 'fully_qualified_url')
|
206
|
+
rescue ArgumentError => ae
|
207
|
+
raise OAuthError.new(ae.message, nil, 'parameter_absent', nil, @realm)
|
208
|
+
end
|
209
|
+
|
210
|
+
query_params = fully_qualified_url.query ? Protocol.parse_url_query_string(fully_qualified_url.query) : {}
|
211
|
+
request_params = query_params.merge(request_params)
|
212
|
+
|
213
|
+
params = request_params.merge(oauth_params)
|
214
|
+
signature_base_string =
|
215
|
+
Signature.build_signature_base_string(
|
216
|
+
http_method: http_method, fully_qualified_url: fully_qualified_url, params: params
|
217
|
+
)
|
218
|
+
|
219
|
+
sig =
|
220
|
+
Signature.sign_via_hmacsha1(
|
221
|
+
client_shared_secret: @accessor_secret,
|
222
|
+
token_shared_secret: @token_secret,
|
223
|
+
signature_base_string: signature_base_string
|
224
|
+
)
|
225
|
+
else
|
226
|
+
raise OAuthError.new('signature_method is invalid', nil, 'signature_method_rejected', nil, @realm)
|
227
|
+
end
|
155
228
|
end
|
156
229
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
oauth_signature: sig,
|
162
|
-
oauth_consumer_key: @consumer_key,
|
163
|
-
oauth_nonce: @nonce,
|
164
|
-
oauth_timestamp: @timestamp.tv_sec,
|
165
|
-
oauth_token: @token
|
166
|
-
}
|
167
|
-
@authorization_header = Protocol.generate_authorization_header(tuples)
|
230
|
+
oauth_params[:realm] = @realm if @realm
|
231
|
+
oauth_params[:oauth_signature] = sig
|
232
|
+
|
233
|
+
Protocol.generate_authorization_header(oauth_params)
|
168
234
|
end
|
169
235
|
|
170
236
|
# Public: Authenticates the #token against the #consumer_key, #signature and side-channel
|
@@ -175,13 +241,27 @@ module Cerner
|
|
175
241
|
# access_token_agent - An instance of Cerner::OAuth1a::AccessTokenAgent configured with
|
176
242
|
# appropriate credentials to retrieve secrets via
|
177
243
|
# Cerner::OAuth1a::AccessTokenAgent#retrieve_keys.
|
244
|
+
# keywords - The keyword arguments:
|
245
|
+
# :http_method - An optional String or Symbol containing an HTTP
|
246
|
+
# method name. (default: 'GET')
|
247
|
+
# :fully_qualified_url - An optional String or URI that contains the
|
248
|
+
# scheme, host, port (optional) and path of a URL.
|
249
|
+
# :request_params - An optional Hash of name/value pairs
|
250
|
+
# representing the request parameters. The keys
|
251
|
+
# and values of the Hash will be assumed to be
|
252
|
+
# represented by the value returned from #to_s.
|
178
253
|
#
|
179
254
|
# Returns a Hash (symbolized keys) of any extra parameters within #token (oauth_token),
|
180
255
|
# if authentication succeeds. In most scenarios, the Hash will be empty.
|
181
256
|
#
|
182
257
|
# Raises ArgumentError if access_token_agent is nil
|
183
258
|
# Raises Cerner::OAuth1a::OAuthError with an oauth_problem if authentication fails.
|
184
|
-
def authenticate(
|
259
|
+
def authenticate(
|
260
|
+
access_token_agent,
|
261
|
+
http_method: 'GET',
|
262
|
+
fully_qualified_url: nil,
|
263
|
+
request_params: nil
|
264
|
+
)
|
185
265
|
raise ArgumentError, 'access_token_agent is nil' unless access_token_agent
|
186
266
|
|
187
267
|
if @realm && !access_token_agent.realm_eql?(@realm)
|
@@ -191,10 +271,6 @@ module Cerner
|
|
191
271
|
# Set realm to the provider's realm if it's not already set
|
192
272
|
@realm ||= access_token_agent.realm
|
193
273
|
|
194
|
-
unless @signature_method == 'PLAINTEXT'
|
195
|
-
raise OAuthError.new('signature_method must be PLAINTEXT', nil, 'signature_method_rejected', nil, @realm)
|
196
|
-
end
|
197
|
-
|
198
274
|
tuples = Protocol.parse_url_query_string(@token)
|
199
275
|
|
200
276
|
unless @consumer_key == tuples.delete(:ConsumerKey)
|
@@ -209,7 +285,13 @@ module Cerner
|
|
209
285
|
# RSASHA1 param gets consumed in #verify_token, so remove it too
|
210
286
|
tuples.delete(:RSASHA1)
|
211
287
|
|
212
|
-
verify_signature(
|
288
|
+
verify_signature(
|
289
|
+
keys: keys,
|
290
|
+
hmac_secrets: tuples.delete(:HMACSecrets),
|
291
|
+
http_method: http_method,
|
292
|
+
fully_qualified_url: fully_qualified_url,
|
293
|
+
request_params: request_params
|
294
|
+
)
|
213
295
|
|
214
296
|
@consumer_principal = tuples.delete(:"Consumer.Principal")
|
215
297
|
|
@@ -231,7 +313,7 @@ module Cerner
|
|
231
313
|
# if @expires_at is nil, return true now
|
232
314
|
return true unless @expires_at
|
233
315
|
|
234
|
-
now = convert_to_time(now)
|
316
|
+
now = Internal.convert_to_time(time: now, name: 'now')
|
235
317
|
now.tv_sec >= @expires_at.tv_sec - fudge_sec
|
236
318
|
end
|
237
319
|
|
@@ -283,59 +365,22 @@ module Cerner
|
|
283
365
|
|
284
366
|
private
|
285
367
|
|
286
|
-
# Internal: Used by #initialize and #expired? to convert data into a Time instance.
|
287
|
-
#
|
288
|
-
# time - Time or any object with a #to_i the returns an Integer.
|
289
|
-
#
|
290
|
-
# Returns a Time instance in the UTC time zone.
|
291
|
-
def convert_to_time(time)
|
292
|
-
raise ArgumentError, 'time is nil' unless time
|
293
|
-
|
294
|
-
if time.is_a? Time
|
295
|
-
time.utc
|
296
|
-
else
|
297
|
-
Time.at(time.to_i).utc
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
368
|
# Internal: Used by #authenticate to verify the expiration time.
|
302
|
-
#
|
303
|
-
# expires_on - The ExpiresOn parameter of oauth_token
|
304
|
-
#
|
305
|
-
# Raises OAuthError if the parameter is invalid or expired
|
306
369
|
def verify_expiration(expires_on)
|
307
370
|
unless expires_on
|
308
|
-
raise OAuthError.new(
|
309
|
-
'token missing ExpiresOn',
|
310
|
-
nil,
|
311
|
-
'oauth_parameters_rejected',
|
312
|
-
'oauth_token',
|
313
|
-
@realm
|
314
|
-
)
|
371
|
+
raise OAuthError.new('token missing ExpiresOn', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
|
315
372
|
end
|
316
373
|
|
317
|
-
expires_on = convert_to_time(expires_on)
|
318
|
-
now = convert_to_time(Time.now)
|
319
|
-
|
320
|
-
|
321
|
-
'token has expired',
|
322
|
-
nil,
|
323
|
-
'token_expired',
|
324
|
-
nil,
|
325
|
-
@realm
|
326
|
-
)
|
327
|
-
end
|
374
|
+
expires_on = Internal.convert_to_time(time: expires_on, name: 'expires_on')
|
375
|
+
now = Internal.convert_to_time(time: Time.now)
|
376
|
+
|
377
|
+
raise OAuthError.new('token has expired', nil, 'token_expired', nil, @realm) if now.tv_sec >= expires_on.tv_sec
|
328
378
|
end
|
329
379
|
|
380
|
+
# Internal: Used by #authenticate to load the keys
|
330
381
|
def load_keys(access_token_agent, keys_version)
|
331
382
|
unless keys_version
|
332
|
-
raise OAuthError.new(
|
333
|
-
'token missing KeysVersion',
|
334
|
-
nil,
|
335
|
-
'oauth_parameters_rejected',
|
336
|
-
'oauth_token',
|
337
|
-
@realm
|
338
|
-
)
|
383
|
+
raise OAuthError.new('token missing KeysVersion', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
|
339
384
|
end
|
340
385
|
|
341
386
|
begin
|
@@ -352,24 +397,14 @@ module Cerner
|
|
352
397
|
end
|
353
398
|
|
354
399
|
# Internal: Used by #authenticate to verify the oauth_token value.
|
355
|
-
#
|
356
|
-
# keys - The Keys instance that contains the key used to sign the oauth_token
|
357
|
-
#
|
358
|
-
# Raises OAuthError if the parameter is not authentic
|
359
400
|
def verify_token(keys)
|
360
|
-
|
361
|
-
|
362
|
-
|
401
|
+
return if keys.verify_rsasha1_signature(@token)
|
402
|
+
|
403
|
+
raise OAuthError.new('token is not authentic', nil, 'oauth_parameters_rejected', 'oauth_token', @realm)
|
363
404
|
end
|
364
405
|
|
365
406
|
# Internal: Used by #authenticate to verify the request signature.
|
366
|
-
|
367
|
-
# keys - The Keys instance that contains the key used to encrypt the HMACSecrets
|
368
|
-
# hmac_secrets - The HMACSecrets parameter of oauth_token
|
369
|
-
#
|
370
|
-
# Raises OAuthError if there is no signature, the parameter is invalid or the signature does
|
371
|
-
# not match the secrets
|
372
|
-
def verify_signature(keys, hmac_secrets)
|
407
|
+
def verify_signature(keys:, hmac_secrets:, http_method:, fully_qualified_url:, request_params:)
|
373
408
|
unless @signature
|
374
409
|
raise OAuthError.new('missing signature', nil, 'oauth_parameters_absent', 'oauth_signature', @realm)
|
375
410
|
end
|
@@ -390,11 +425,58 @@ module Cerner
|
|
390
425
|
end
|
391
426
|
|
392
427
|
secrets_parts = Protocol.parse_url_query_string(secrets)
|
393
|
-
expected_signature = "#{secrets_parts[:ConsumerSecret]}&#{secrets_parts[:TokenSecret]}"
|
394
428
|
|
395
|
-
|
396
|
-
|
429
|
+
if @signature_method == 'PLAINTEXT'
|
430
|
+
expected_signature =
|
431
|
+
Signature.sign_via_plaintext(
|
432
|
+
client_shared_secret: secrets_parts[:ConsumerSecret], token_shared_secret: secrets_parts[:TokenSecret]
|
433
|
+
)
|
434
|
+
elsif @signature_method == 'HMAC-SHA1'
|
435
|
+
http_method ||= 'GET' # default to HTTP GET
|
436
|
+
request_params ||= {} # default to no request params
|
437
|
+
oauth_params = {
|
438
|
+
oauth_version: '1.0', # assumes version is present
|
439
|
+
oauth_signature_method: 'HMAC-SHA1',
|
440
|
+
oauth_consumer_key: @consumer_key,
|
441
|
+
oauth_nonce: @nonce,
|
442
|
+
oauth_timestamp: @timestamp.to_i,
|
443
|
+
oauth_token: @token
|
444
|
+
}
|
445
|
+
|
446
|
+
begin
|
447
|
+
fully_qualified_url = Internal.convert_to_http_uri(url: fully_qualified_url, name: 'fully_qualified_url')
|
448
|
+
rescue ArgumentError => ae
|
449
|
+
raise OAuthError.new(ae.message, nil, 'parameter_absent', nil, @realm)
|
450
|
+
end
|
451
|
+
|
452
|
+
query_params = fully_qualified_url.query ? Protocol.parse_url_query_string(fully_qualified_url.query) : {}
|
453
|
+
request_params = query_params.merge(request_params)
|
454
|
+
|
455
|
+
params = request_params.merge(oauth_params)
|
456
|
+
signature_base_string =
|
457
|
+
Signature.build_signature_base_string(
|
458
|
+
http_method: http_method, fully_qualified_url: fully_qualified_url, params: params
|
459
|
+
)
|
460
|
+
|
461
|
+
expected_signature =
|
462
|
+
Signature.sign_via_hmacsha1(
|
463
|
+
client_shared_secret: secrets_parts[:ConsumerSecret],
|
464
|
+
token_shared_secret: secrets_parts[:TokenSecret],
|
465
|
+
signature_base_string: signature_base_string
|
466
|
+
)
|
467
|
+
else
|
468
|
+
raise OAuthError.new(
|
469
|
+
'signature_method must be PLAINTEXT or HMAC-SHA1',
|
470
|
+
nil,
|
471
|
+
'signature_method_rejected',
|
472
|
+
nil,
|
473
|
+
@realm
|
474
|
+
)
|
397
475
|
end
|
476
|
+
|
477
|
+
return if Internal.constant_time_compare(@signature, expected_signature)
|
478
|
+
|
479
|
+
raise OAuthError.new('signature is not valid', nil, 'signature_invalid', nil, @realm)
|
398
480
|
end
|
399
481
|
end
|
400
482
|
end
|