cerner-oauth1a 1.0.1 → 2.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/CHANGELOG.md +9 -0
- data/NOTICE +1 -1
- data/README.md +77 -23
- data/lib/cerner/oauth1a.rb +4 -0
- data/lib/cerner/oauth1a/access_token.rb +224 -41
- data/lib/cerner/oauth1a/access_token_agent.rb +194 -87
- data/lib/cerner/oauth1a/cache.rb +79 -0
- data/lib/cerner/oauth1a/keys.rb +124 -0
- data/lib/cerner/oauth1a/oauth_error.rb +72 -6
- data/lib/cerner/oauth1a/protocol.rb +132 -0
- data/lib/cerner/oauth1a/version.rb +3 -1
- metadata +14 -9
@@ -1,15 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
1
4
|
require 'cerner/oauth1a/access_token'
|
5
|
+
require 'cerner/oauth1a/keys'
|
2
6
|
require 'cerner/oauth1a/oauth_error'
|
7
|
+
require 'cerner/oauth1a/cache'
|
8
|
+
require 'cerner/oauth1a/protocol'
|
3
9
|
require 'cerner/oauth1a/version'
|
10
|
+
require 'json'
|
4
11
|
require 'net/https'
|
5
12
|
require 'securerandom'
|
6
13
|
require 'uri'
|
7
14
|
|
8
15
|
module Cerner
|
9
16
|
module OAuth1a
|
10
|
-
|
11
|
-
#
|
12
|
-
# Access Tokens.
|
17
|
+
# Public: A user agent for interacting with the Cerner OAuth 1.0a Access Token service to acquire
|
18
|
+
# consumer Access Tokens or service provider Keys.
|
13
19
|
class AccessTokenAgent
|
14
20
|
MIME_WWW_FORM_URL_ENCODED = 'application/x-www-form-urlencoded'
|
15
21
|
|
@@ -22,18 +28,41 @@ module Cerner
|
|
22
28
|
|
23
29
|
# Public: Constructs an instance of the agent.
|
24
30
|
#
|
31
|
+
# Caching - By default, AccessToken and Keys instances are maintained in a small, constrained
|
32
|
+
# memory cache used by #retrieve and #retrieve_keys, respectively.
|
33
|
+
#
|
34
|
+
# The AccessToken cache keeps a maximum of 5 entries and prunes them when they expire. As the
|
35
|
+
# cache is based on the #consumer_key and the 'principal' parameter, the cache has limited
|
36
|
+
# effect. It's strongly suggested that AccessToken's be cached independently, as well.
|
37
|
+
#
|
38
|
+
# The Keys cache keeps a maximum of 10 entries and prunes them 24 hours after retrieval.
|
39
|
+
#
|
25
40
|
# arguments - The keyword arguments of the method:
|
26
|
-
# :access_token_url
|
27
|
-
# :consumer_key
|
28
|
-
# :consumer_secret
|
29
|
-
# :open_timeout
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
|
41
|
+
# :access_token_url - The String or URI of the Access Token service endpoint.
|
42
|
+
# :consumer_key - The String of the Consumer Key of the account.
|
43
|
+
# :consumer_secret - The String of the Consumer Secret of the account.
|
44
|
+
# :open_timeout - An object responding to to_i. Used to set the timeout, in
|
45
|
+
# seconds, for opening HTTP connections to the Access Token
|
46
|
+
# service (optional, default: 5).
|
47
|
+
# :read_timeout - An object responding to to_i. Used to set the timeout, in
|
48
|
+
# seconds, for reading data from HTTP connections to the
|
49
|
+
# Access Token service (optional, default: 5).
|
50
|
+
# :cache_keys - A Boolean for configuring Keys caching within
|
51
|
+
# #retrieve_keys. (optional, default: true)
|
52
|
+
# :cache_access_tokens - A Boolean for configuring AccessToken caching within
|
53
|
+
# #retrieve. (optional, default: true)
|
54
|
+
#
|
55
|
+
# Raises ArgumentError if access_token_url, consumer_key or consumer_key is nil; if
|
56
|
+
# access_token_url is an invalid URI.
|
57
|
+
def initialize(
|
58
|
+
access_token_url:,
|
59
|
+
consumer_key:,
|
60
|
+
consumer_secret:,
|
61
|
+
open_timeout: 5,
|
62
|
+
read_timeout: 5,
|
63
|
+
cache_keys: true,
|
64
|
+
cache_access_tokens: true
|
65
|
+
)
|
37
66
|
raise ArgumentError, 'consumer_key is nil' unless consumer_key
|
38
67
|
raise ArgumentError, 'consumer_secret is nil' unless consumer_secret
|
39
68
|
|
@@ -44,73 +73,65 @@ module Cerner
|
|
44
73
|
|
45
74
|
@open_timeout = (open_timeout ? open_timeout.to_i : 5)
|
46
75
|
@read_timeout = (read_timeout ? read_timeout.to_i : 5)
|
76
|
+
|
77
|
+
@keys_cache = cache_keys ? Cache.new(max: 10) : nil
|
78
|
+
@access_token_cache = cache_access_tokens ? Cache.new(max: 5) : nil
|
47
79
|
end
|
48
80
|
|
49
|
-
# Public:
|
50
|
-
# This method will
|
51
|
-
#
|
81
|
+
# Public: Retrieves the service provider keys from the configured Access Token service endpoint
|
82
|
+
# (@access_token_url). This method will invoke #retrieve to acquire an AccessToken to request
|
83
|
+
# the keys.
|
52
84
|
#
|
53
|
-
#
|
85
|
+
# keys_version - The version identifier of the keys to retrieve. This corresponds to the
|
86
|
+
# KeysVersion parameter of the oauth_token.
|
54
87
|
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
88
|
+
# Return a Keys instance upon success.
|
89
|
+
#
|
90
|
+
# Raises ArgumentError if keys_version is nil.
|
91
|
+
# Raises OAuthError for any functional errors returned within an HTTP 200 response.
|
92
|
+
# Raises StandardError sub-classes for any issues interacting with the service, such as networking issues.
|
93
|
+
def retrieve_keys(keys_version)
|
94
|
+
raise ArgumentError, 'keys_version is nil' unless keys_version
|
60
95
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
params = [
|
66
|
-
[:oauth_consumer_key, @consumer_key],
|
67
|
-
[:oauth_signature_method, 'PLAINTEXT'],
|
68
|
-
[:oauth_version, '1.0'],
|
69
|
-
[:oauth_timestamp, timestamp],
|
70
|
-
[:oauth_nonce, nonce],
|
71
|
-
[:oauth_signature, "#{@consumer_secret}&"],
|
72
|
-
[:oauth_accessor_secret, accessor_secret]
|
73
|
-
]
|
74
|
-
# set the POST's body as a URL form-encoded string
|
75
|
-
request.set_form(params, MIME_WWW_FORM_URL_ENCODED, charset: 'UTF-8')
|
96
|
+
if @keys_cache
|
97
|
+
cache_entry = @keys_cache.get(keys_version)
|
98
|
+
return cache_entry.value if cache_entry
|
99
|
+
end
|
76
100
|
|
77
|
-
request
|
78
|
-
|
79
|
-
|
101
|
+
request = retrieve_keys_prepare_request(keys_version)
|
102
|
+
response = http_client.request(request)
|
103
|
+
keys = retrieve_keys_handle_response(keys_version, response)
|
104
|
+
@keys_cache&.put(keys_version, Cache::KeysEntry.new(keys, Cache::TWENTY_FOUR_HOURS))
|
105
|
+
keys
|
106
|
+
end
|
80
107
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
108
|
+
# Public: Retrieves an AccessToken from the configured Access Token service endpoint (#access_token_url).
|
109
|
+
# This method will use the #generate_accessor_secret, #generate_nonce and #generate_timestamp methods to
|
110
|
+
# interact with the service, which can be overridden via a sub-class, if desired.
|
111
|
+
#
|
112
|
+
# principal - An optional principal identifier, which is passed via the xoauth_principal protocol parameter.
|
113
|
+
#
|
114
|
+
# Returns a AccessToken upon success.
|
115
|
+
#
|
116
|
+
# Raises OAuthError for any functional errors returned within an HTTP 200 response.
|
117
|
+
# Raises StandardError sub-classes for any issues interacting with the service, such as networking issues.
|
118
|
+
def retrieve(principal = nil)
|
119
|
+
cache_key = "#{@consumer_key}&#{principal}"
|
120
|
+
if @access_token_cache
|
121
|
+
cache_entry = @access_token_cache.get(cache_key)
|
122
|
+
return cache_entry.value if cache_entry
|
89
123
|
end
|
90
|
-
http.open_timeout = @open_timeout
|
91
|
-
http.read_timeout = @read_timeout
|
92
124
|
|
93
|
-
|
125
|
+
# generate token request info
|
126
|
+
nonce = generate_nonce
|
127
|
+
timestamp = generate_timestamp
|
128
|
+
accessor_secret = generate_accessor_secret
|
94
129
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
access_token = AccessToken.new(accessor_secret: accessor_secret,
|
101
|
-
consumer_key: @consumer_key,
|
102
|
-
expires_at: timestamp + tuples[:oauth_expires_in].to_i,
|
103
|
-
nonce: nonce,
|
104
|
-
timestamp: timestamp,
|
105
|
-
token: tuples[:oauth_token],
|
106
|
-
token_secret: tuples[:oauth_token_secret])
|
107
|
-
access_token
|
108
|
-
else
|
109
|
-
# Extract any OAuth Problems reported in the response
|
110
|
-
oauth_data = parse_www_authenticate(response['WWW-Authenticate'])
|
111
|
-
# Raise an error for a failure to acquire a token
|
112
|
-
raise OAuthError.new('unable to acquire token', response.code, oauth_data['oauth_problem'])
|
113
|
-
end
|
130
|
+
request = retrieve_prepare_request(timestamp, nonce, accessor_secret, principal)
|
131
|
+
response = http_client.request(request)
|
132
|
+
access_token = retrieve_handle_response(response, timestamp, nonce, accessor_secret)
|
133
|
+
@access_token_cache&.put(cache_key, Cache::AccessTokenEntry.new(access_token))
|
134
|
+
access_token
|
114
135
|
end
|
115
136
|
|
116
137
|
# Public: Generate an Accessor Secret for invocations of the Access Token service.
|
@@ -136,18 +157,28 @@ module Cerner
|
|
136
157
|
|
137
158
|
private
|
138
159
|
|
139
|
-
# Internal:
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
#
|
145
|
-
def
|
146
|
-
|
147
|
-
|
148
|
-
|
160
|
+
# Internal: Generate a User-Agent HTTP Header string
|
161
|
+
def user_agent_string
|
162
|
+
"cerner-oauth1a #{VERSION} (Ruby #{RUBY_VERSION})"
|
163
|
+
end
|
164
|
+
|
165
|
+
# Internal: Provide the HTTP client instance for invoking requests
|
166
|
+
def http_client
|
167
|
+
http = Net::HTTP.new(@access_token_url.host, @access_token_url.port)
|
168
|
+
|
169
|
+
if @access_token_url.scheme == 'https'
|
170
|
+
# if the scheme is HTTPS, then enable SSL
|
171
|
+
http.use_ssl = true
|
172
|
+
# make sure to verify peers
|
173
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
174
|
+
# tweak the ciphers to eliminate unsafe options
|
175
|
+
http.ciphers = 'DEFAULT:!aNULL:!eNULL:!LOW:!SSLv2:!RC4'
|
176
|
+
end
|
177
|
+
|
178
|
+
http.open_timeout = @open_timeout
|
179
|
+
http.read_timeout = @read_timeout
|
149
180
|
|
150
|
-
|
181
|
+
http
|
151
182
|
end
|
152
183
|
|
153
184
|
# Internal: Convert an Access Token URL into a URI with some verification checks
|
@@ -163,17 +194,93 @@ module Cerner
|
|
163
194
|
else
|
164
195
|
begin
|
165
196
|
uri = URI(access_token_url)
|
166
|
-
rescue URI::InvalidURIError
|
197
|
+
rescue URI::InvalidURIError
|
167
198
|
# raise argument error with cause
|
168
199
|
raise ArgumentError, 'access_token_url is invalid'
|
169
200
|
end
|
170
201
|
end
|
171
|
-
unless uri.is_a?
|
172
|
-
raise ArgumentError, 'access_token_url must be an HTTP or HTTPS URI'
|
173
|
-
end
|
202
|
+
raise ArgumentError, 'access_token_url must be an HTTP or HTTPS URI' unless uri.is_a?(URI::HTTP)
|
174
203
|
uri
|
175
204
|
end
|
176
|
-
end
|
177
205
|
|
206
|
+
# Internal: Prepare a request for #retrieve
|
207
|
+
def retrieve_prepare_request(timestamp, nonce, accessor_secret, principal)
|
208
|
+
# construct a POST request
|
209
|
+
request = Net::HTTP::Post.new(@access_token_url)
|
210
|
+
# setup the data to construct the POST's message
|
211
|
+
params = [
|
212
|
+
[:oauth_consumer_key, @consumer_key],
|
213
|
+
[:oauth_signature_method, 'PLAINTEXT'],
|
214
|
+
[:oauth_version, '1.0'],
|
215
|
+
[:oauth_timestamp, timestamp],
|
216
|
+
[:oauth_nonce, nonce],
|
217
|
+
[:oauth_signature, "#{@consumer_secret}&"],
|
218
|
+
[:oauth_accessor_secret, accessor_secret]
|
219
|
+
]
|
220
|
+
params << [:xoauth_principal, principal.to_s] if principal
|
221
|
+
# set the POST's body as a URL form-encoded string
|
222
|
+
request.set_form(params, MIME_WWW_FORM_URL_ENCODED, charset: 'UTF-8')
|
223
|
+
request['Accept'] = MIME_WWW_FORM_URL_ENCODED
|
224
|
+
# Set a custom User-Agent to help identify these invocation
|
225
|
+
request['User-Agent'] = user_agent_string
|
226
|
+
request
|
227
|
+
end
|
228
|
+
|
229
|
+
# Internal: Handle a response for #retrieve
|
230
|
+
def retrieve_handle_response(response, timestamp, nonce, accessor_secret)
|
231
|
+
case response
|
232
|
+
when Net::HTTPSuccess
|
233
|
+
# Parse the HTTP response and convert it into a Symbol-keyed Hash
|
234
|
+
tuples = Protocol.parse_url_query_string(response.body)
|
235
|
+
# Use the parsed response to construct the AccessToken
|
236
|
+
access_token = AccessToken.new(
|
237
|
+
accessor_secret: accessor_secret,
|
238
|
+
consumer_key: @consumer_key,
|
239
|
+
expires_at: timestamp + tuples[:oauth_expires_in].to_i,
|
240
|
+
nonce: nonce,
|
241
|
+
timestamp: timestamp,
|
242
|
+
token: tuples[:oauth_token],
|
243
|
+
token_secret: tuples[:oauth_token_secret]
|
244
|
+
)
|
245
|
+
access_token
|
246
|
+
else
|
247
|
+
# Extract any OAuth Problems reported in the response
|
248
|
+
oauth_data = Protocol.parse_authorization_header(response['WWW-Authenticate'])
|
249
|
+
# Raise an error for a failure to acquire a token
|
250
|
+
raise OAuthError.new('unable to acquire token', response.code, oauth_data[:oauth_problem])
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Internal: Prepare a request for #retrieve_keys
|
255
|
+
def retrieve_keys_prepare_request(keys_version)
|
256
|
+
request = Net::HTTP::Get.new("#{@access_token_url}/keys/#{keys_version}")
|
257
|
+
request['Accept'] = 'application/json'
|
258
|
+
request['User-Agent'] = user_agent_string
|
259
|
+
request['Authorization'] = retrieve.authorization_header
|
260
|
+
request
|
261
|
+
end
|
262
|
+
|
263
|
+
# Internal: Handle a response for #retrieve_keys
|
264
|
+
def retrieve_keys_handle_response(keys_version, response)
|
265
|
+
case response
|
266
|
+
when Net::HTTPSuccess
|
267
|
+
parsed_response = JSON.parse(response.body)
|
268
|
+
aes_key = parsed_response.dig('aesKey', 'secretKey')
|
269
|
+
raise OAuthError, 'AES secret key retrieved was invalid' unless aes_key
|
270
|
+
rsa_key = parsed_response.dig('rsaKey', 'publicKey')
|
271
|
+
raise OAuthError, 'RSA public key retrieved was invalid' unless rsa_key
|
272
|
+
Keys.new(
|
273
|
+
version: keys_version,
|
274
|
+
aes_secret_key: Base64.decode64(aes_key),
|
275
|
+
rsa_public_key: Base64.decode64(rsa_key)
|
276
|
+
)
|
277
|
+
else
|
278
|
+
# Extract any OAuth Problems reported in the response
|
279
|
+
oauth_data = Protocol.parse_authorization_header(response['WWW-Authenticate'])
|
280
|
+
# Raise an error for a failure to acquire keys
|
281
|
+
raise OAuthError.new('unable to acquire keys', response.code, oauth_data[:oauth_problem])
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
178
285
|
end
|
179
286
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cerner
|
4
|
+
module OAuth1a
|
5
|
+
# Internal: A simple cache abstraction for use by AccessTokenAgent only.
|
6
|
+
class Cache
|
7
|
+
# Internal: A cache entry class for Keys values.
|
8
|
+
class KeysEntry
|
9
|
+
attr_reader :value
|
10
|
+
|
11
|
+
def initialize(keys, expires_in)
|
12
|
+
@value = keys
|
13
|
+
@expires_at = Time.now.utc.to_i + expires_in
|
14
|
+
end
|
15
|
+
|
16
|
+
def expired?(now)
|
17
|
+
@expires_at <= now
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Internal: A cache entry class for AccessToken values.
|
22
|
+
class AccessTokenEntry
|
23
|
+
attr_reader :value
|
24
|
+
|
25
|
+
def initialize(access_token)
|
26
|
+
@value = access_token
|
27
|
+
end
|
28
|
+
|
29
|
+
def expired?(now)
|
30
|
+
@value.expired?(now: now)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
ONE_HOUR = 3600
|
35
|
+
TWENTY_FOUR_HOURS = 24 * ONE_HOUR
|
36
|
+
|
37
|
+
def initialize(max:)
|
38
|
+
@max = max
|
39
|
+
@lock = Mutex.new
|
40
|
+
@entries = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def put(key, entry)
|
44
|
+
@lock.synchronize do
|
45
|
+
now = Time.now.utc.to_i
|
46
|
+
prune_expired(now)
|
47
|
+
@entries[key] = entry
|
48
|
+
prune_size
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def get(key)
|
53
|
+
@lock.synchronize do
|
54
|
+
prune_expired(Time.now.utc.to_i)
|
55
|
+
@entries[key]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def prune_expired(now)
|
62
|
+
return if @entries.empty?
|
63
|
+
|
64
|
+
@entries.delete_if { |_, v| v.expired?(now) }
|
65
|
+
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def prune_size
|
70
|
+
return if @entries.empty? || @entries.size <= @max
|
71
|
+
|
72
|
+
num_to_prune = @entries.size - @max
|
73
|
+
num_to_prune.times { @entries.shift }
|
74
|
+
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'openssl'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
module Cerner
|
8
|
+
module OAuth1a
|
9
|
+
# Public: Keys for authenticating Access Tokens by service providers. Keys can be retrieved
|
10
|
+
# via AccessTokenAgent#retrieve_keys.
|
11
|
+
class Keys
|
12
|
+
# Returns the String version identifier of the keys.
|
13
|
+
attr_reader :version
|
14
|
+
# Returns the String AES secret key.
|
15
|
+
attr_reader :aes_secret_key
|
16
|
+
# Returns the String RSA public key.
|
17
|
+
attr_reader :rsa_public_key
|
18
|
+
|
19
|
+
# Public: Constructs an instance.
|
20
|
+
#
|
21
|
+
# arguments - The keyword arguments of the method:
|
22
|
+
# :version - The version identifier of the keys.
|
23
|
+
# :aes_secret_key - The AES secret key.
|
24
|
+
# :rsa_public_key - The RSA public key.
|
25
|
+
#
|
26
|
+
# Raises ArgumentError if version, aes_secret_key or rsa_public_key is nil.
|
27
|
+
def initialize(version:, aes_secret_key:, rsa_public_key:)
|
28
|
+
raise ArgumentError, 'version is nil' unless version
|
29
|
+
raise ArgumentError, 'aes_secret_key is nil' unless aes_secret_key
|
30
|
+
raise ArgumentError, 'rsa_public_key is nil' unless rsa_public_key
|
31
|
+
|
32
|
+
@version = version
|
33
|
+
@aes_secret_key = aes_secret_key
|
34
|
+
@rsa_public_key = rsa_public_key
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Compare this to other based on attributes.
|
38
|
+
#
|
39
|
+
# other - The Keys to compare this to.
|
40
|
+
#
|
41
|
+
# Return true if equal; false otherwise
|
42
|
+
def ==(other)
|
43
|
+
version == other.version &&
|
44
|
+
aes_secret_key == other.aes_secret_key &&
|
45
|
+
rsa_public_key == other.rsa_public_key
|
46
|
+
end
|
47
|
+
|
48
|
+
# Public: Compare this to other based on attributes.
|
49
|
+
#
|
50
|
+
# other - The Keys to compare this to.
|
51
|
+
#
|
52
|
+
# Return true if equal; false otherwise
|
53
|
+
def eql?(other)
|
54
|
+
self == other
|
55
|
+
end
|
56
|
+
|
57
|
+
# Public: Generates a Hash of the attributes.
|
58
|
+
#
|
59
|
+
# Returns a Hash with keys for each attribute.
|
60
|
+
def to_h
|
61
|
+
{
|
62
|
+
version: @version,
|
63
|
+
aes_secret_key: @aes_secret_key,
|
64
|
+
rsa_public_key: @rsa_public_key
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Public: Returns the #rsa_public_key as an OpenSSL::PKey::RSA intance.
|
69
|
+
#
|
70
|
+
# Raises OpenSSL::PKey::RSAError if #rsa_public_key is not a valid key
|
71
|
+
def rsa_public_key_as_pkey
|
72
|
+
OpenSSL::PKey::RSA.new(@rsa_public_key)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Public: Verifies that an oauth_token is authentic based on the #rsa_public_key.
|
76
|
+
#
|
77
|
+
# oauth_token - The oauth_token value to verify.
|
78
|
+
#
|
79
|
+
# Returns true if authentic; false otherwise.
|
80
|
+
#
|
81
|
+
# Raises ArgumentError if oauth_token is nil or invalid
|
82
|
+
# Raises OpenSSL::PKey::RSAError if #rsa_public_key is not a valid key
|
83
|
+
def verify_rsasha1_signature(oauth_token)
|
84
|
+
raise ArgumentError, 'oauth_token is nil' unless oauth_token
|
85
|
+
|
86
|
+
message, raw_sig = oauth_token.split('&RSASHA1=')
|
87
|
+
raise ArgumentError, 'unable to get message out of oauth_token' unless message
|
88
|
+
raise ArgumentError, 'unable to get RSASHA1 signature out of oauth_token' unless raw_sig
|
89
|
+
|
90
|
+
# URL decode value and Base64 (urlsafe) decode that result
|
91
|
+
sig = Base64.urlsafe_decode64(URI.decode_www_form_component(raw_sig))
|
92
|
+
rsa_public_key_as_pkey.verify(OpenSSL::Digest::SHA1.new, sig, message)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Public: Decrypts the HMACSecrets parameter of an oauth_token using the #aes_secret_key.
|
96
|
+
#
|
97
|
+
# hmac_secrets_param - The extracted value of the HMACSecrets parameter of an oauth_token. The
|
98
|
+
# value is assumed to be Base64 (URL safe) encoded.
|
99
|
+
#
|
100
|
+
# Returns the decrypted secrets.
|
101
|
+
#
|
102
|
+
# Raises ArgumentError if oauth_token is nil or invalid
|
103
|
+
def decrypt_hmac_secrets(hmac_secrets_param)
|
104
|
+
raise ArgumentError, 'hmac_secrets_param is nil' unless hmac_secrets_param
|
105
|
+
|
106
|
+
ciphertext = Base64.urlsafe_decode64(hmac_secrets_param)
|
107
|
+
raise ArgumentError, 'hmac_secrets_param does not contain enough data' unless ciphertext.size > 16
|
108
|
+
|
109
|
+
# extract first 16 bytes to get initialization vector
|
110
|
+
iv = ciphertext[0, 16]
|
111
|
+
# trim off the IV
|
112
|
+
ciphertext = ciphertext[16..-1]
|
113
|
+
|
114
|
+
cipher = OpenSSL::Cipher.new('AES-128-CBC')
|
115
|
+
# invoke #decrypt to prep the instance
|
116
|
+
cipher.decrypt
|
117
|
+
cipher.iv = iv
|
118
|
+
cipher.key = @aes_secret_key
|
119
|
+
text = cipher.update(ciphertext) + cipher.final
|
120
|
+
text
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|