mastercard_oauth1_signer 1.0.0
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 +7 -0
- data/lib/oauth.rb +253 -0
- data/oauth1_signer_ruby.gemspec +16 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0556b0ec00889b34169f2e9df97f03fa310902a80f53a3e9b2e7a3c6b0f40a92
|
4
|
+
data.tar.gz: 06ac83fc0b6d244db8d0add6b0d0c62486cca6101d2b478edb01e3b5351a1c90
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c3b44697f8f1a541da01347b1e1cd262abd2addd84f8417115ed6876f7c58af0e4741aafaac98108706ac530492d165e092361bba76e42d26113465139901031
|
7
|
+
data.tar.gz: 06cbae148cd7c605545a910760a3554bba080afd4313a16cbcd49b822aa0d43ec33ba49f819d0e285c80b9a88cc347afa6918776229e0bbb18331845e4148b66
|
data/lib/oauth.rb
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'openssl'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
class OAuth
|
6
|
+
class << self
|
7
|
+
|
8
|
+
EMPTY_STRING = ''.freeze
|
9
|
+
SHA_BITS = '256'.freeze
|
10
|
+
|
11
|
+
# Creates a Mastercard API compliant OAuth Authorization header
|
12
|
+
#
|
13
|
+
# @param {String} uri Target URI for this request
|
14
|
+
# @param {String} method HTTP method of the request
|
15
|
+
# @param {Any} payload Payload (nullable)
|
16
|
+
# @param {String} consumerKey Consumer key set up in a Mastercard Developer Portal project
|
17
|
+
# @param {String} signingKey The private key that will be used for signing the request that corresponds to the consumerKey
|
18
|
+
# @return {String} Valid OAuth1.0a signature with a body hash when payload is present
|
19
|
+
#
|
20
|
+
def get_authorization_header(uri, method, payload, consumer_key, signing_key)
|
21
|
+
query_params = extract_query_params(uri)
|
22
|
+
oauth_params = get_oauth_params(consumer_key, payload)
|
23
|
+
|
24
|
+
# Combine query and oauth_ parameters into lexicographically sorted string
|
25
|
+
param_string = to_oauth_param_string(query_params, oauth_params)
|
26
|
+
|
27
|
+
# Normalized URI without query params and fragment
|
28
|
+
base_uri = get_base_uri_string(uri)
|
29
|
+
|
30
|
+
# Signature base string
|
31
|
+
sbs = get_signature_base_string(method, base_uri, param_string)
|
32
|
+
|
33
|
+
# Signature
|
34
|
+
signature = sign_signature_base_string(sbs, signing_key)
|
35
|
+
oauth_params['oauth_signature'] = encode_uri_component(signature)
|
36
|
+
|
37
|
+
# Return
|
38
|
+
get_authorization_string(oauth_params)
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Parse query parameters out of the URL.
|
43
|
+
# https://tools.ietf.org/html/rfc5849#section-3.4.1.3
|
44
|
+
#
|
45
|
+
# @param {String} uri URL containing all query parameters that need to be signed
|
46
|
+
# @return {Map<String, Set<String>} Sorted map of query parameter key/value pairs. Values for parameters with the same name are added into a list.
|
47
|
+
#
|
48
|
+
def extract_query_params(uri)
|
49
|
+
query_params = URI.parse(uri).query
|
50
|
+
|
51
|
+
return {} if query_params.eql?(nil)
|
52
|
+
|
53
|
+
query_pairs = {}
|
54
|
+
pairs = query_params.split('&').sort_by(&:downcase)
|
55
|
+
|
56
|
+
pairs.each { |pair|
|
57
|
+
idx = pair.index('=')
|
58
|
+
key = idx > 0 ? pair[0..(idx - 1)] : pair
|
59
|
+
query_pairs[key] = [] unless query_pairs.include?(key)
|
60
|
+
value = if idx > 0 && pair.length > idx + 1
|
61
|
+
pair[(idx + 1)..pair.length]
|
62
|
+
else
|
63
|
+
EMPTY_STRING
|
64
|
+
end
|
65
|
+
query_pairs[key].push(value)
|
66
|
+
}
|
67
|
+
query_pairs
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# @param {String} consumerKey Consumer key set up in a Mastercard Developer Portal project
|
72
|
+
# @param {Any} payload Payload (nullable)
|
73
|
+
# @return {Map}
|
74
|
+
#
|
75
|
+
def get_oauth_params(consumer_key, payload = nil)
|
76
|
+
oauth_params = {}
|
77
|
+
|
78
|
+
unless payload.nil?
|
79
|
+
oauth_params['oauth_body_hash'] = get_body_hash(payload)
|
80
|
+
end
|
81
|
+
oauth_params['oauth_consumer_key'] = consumer_key
|
82
|
+
oauth_params['oauth_nonce'] = get_nonce
|
83
|
+
oauth_params['oauth_signature_method'] = "RSA-SHA#{SHA_BITS}"
|
84
|
+
oauth_params['oauth_timestamp'] = time_stamp
|
85
|
+
oauth_params['oauth_version'] = '1.0'
|
86
|
+
|
87
|
+
oauth_params
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Constructs a valid Authorization header as per
|
92
|
+
# https://tools.ietf.org/html/rfc5849#section-3.5.1
|
93
|
+
# @param {Map<String, String>} oauthParams Map of OAuth parameters to be included in the Authorization header
|
94
|
+
# @return {String} Correctly formatted header
|
95
|
+
#
|
96
|
+
def get_authorization_string(oauth_params)
|
97
|
+
header = 'OAuth '
|
98
|
+
oauth_params.each {|entry|
|
99
|
+
entry_key = entry[0]
|
100
|
+
entry_val = entry[1]
|
101
|
+
header = "#{header}#{entry_key} = '#{entry_val}',"
|
102
|
+
}
|
103
|
+
# Remove trailing ,
|
104
|
+
header.slice(0, header.length - 1)
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Normalizes the URL as per
|
109
|
+
# https://tools.ietf.org/html/rfc5849#section-3.4.1.2
|
110
|
+
#
|
111
|
+
# @param {String} uri URL that will be called as part of this request
|
112
|
+
# @return {String} Normalized URL
|
113
|
+
#
|
114
|
+
def get_base_uri_string(uri)
|
115
|
+
url = URI.parse(uri)
|
116
|
+
# Lowercase scheme and authority
|
117
|
+
# Remove query and fragment
|
118
|
+
"#{url.scheme.downcase}://#{url.host.downcase}#{url.path.downcase}"
|
119
|
+
end
|
120
|
+
|
121
|
+
#
|
122
|
+
# Lexicographically sort all parameters and concatenate them into a string as per
|
123
|
+
# https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
|
124
|
+
#
|
125
|
+
# @param {Map<String, Set<String>>} queryParamsMap Map of all oauth parameters that need to be signed
|
126
|
+
# @param {Map<String, String>} oauthParamsMap Map of OAuth parameters to be included in Authorization header
|
127
|
+
# @return {String} Correctly encoded and sorted OAuth parameter string
|
128
|
+
#
|
129
|
+
|
130
|
+
def to_oauth_param_string(query_params_map, oauth_param_map)
|
131
|
+
consolidated_params = {}.merge(query_params_map)
|
132
|
+
|
133
|
+
# Add OAuth params to consolidated params map
|
134
|
+
oauth_param_map.each {|entry|
|
135
|
+
entry_key = entry[0]
|
136
|
+
entry_val = entry[1]
|
137
|
+
consolidated_params[entry_key] =
|
138
|
+
if consolidated_params.include?(entry_key)
|
139
|
+
entry_val
|
140
|
+
else
|
141
|
+
[].push(entry_val)
|
142
|
+
end
|
143
|
+
}
|
144
|
+
|
145
|
+
oauth_params = ''
|
146
|
+
|
147
|
+
# Add all parameters to the parameter string for signing
|
148
|
+
consolidated_params.each do |entry|
|
149
|
+
entry_key = entry[0]
|
150
|
+
entry_value = entry[1]
|
151
|
+
|
152
|
+
# Keys with same name are sorted by their values
|
153
|
+
entry_value = entry_value.sort if entry_value.size > 1
|
154
|
+
|
155
|
+
entry_value.each do |value|
|
156
|
+
oauth_params += "#{entry_key}=#{value}&"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Remove trailing ampersand
|
161
|
+
string_length = oauth_params.length - 1
|
162
|
+
if oauth_params.end_with?('&')
|
163
|
+
oauth_params = oauth_params.slice(0, string_length)
|
164
|
+
end
|
165
|
+
|
166
|
+
oauth_params
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# Generate a valid signature base string as per
|
171
|
+
# https://tools.ietf.org/html/rfc5849#section-3.4.1
|
172
|
+
#
|
173
|
+
# @param {String} httpMethod HTTP method of the request
|
174
|
+
# @param {String} baseUri Base URI that conforms with https://tools.ietf.org/html/rfc5849#section-3.4.1.2
|
175
|
+
# @param {String} paramString OAuth parameter string that conforms with https://tools.ietf.org/html/rfc5849#section-3.4.1.3
|
176
|
+
# @return {String} A correctly constructed and escaped signature base string
|
177
|
+
#
|
178
|
+
def get_signature_base_string(http_method, base_uri, param_string)
|
179
|
+
sbs =
|
180
|
+
# Uppercase HTTP method
|
181
|
+
"#{http_method.upcase}&" +
|
182
|
+
# Base URI
|
183
|
+
"#{encode_uri_component(base_uri)}&" +
|
184
|
+
# OAuth parameter string
|
185
|
+
encode_uri_component(param_string).to_s
|
186
|
+
|
187
|
+
sbs.gsub(/!/, '%21')
|
188
|
+
end
|
189
|
+
|
190
|
+
#
|
191
|
+
# Signs the signature base string using an RSA private key. The methodology is described at
|
192
|
+
# https://tools.ietf.org/html/rfc5849#section-3.4.3 but Mastercard uses the stronger SHA-256 algorithm
|
193
|
+
# as a replacement for the described SHA1 which is no longer considered secure.
|
194
|
+
#
|
195
|
+
# @param {String} sbs Signature base string formatted as per https://tools.ietf.org/html/rfc5849#section-3.4.1
|
196
|
+
# @param {String} signingKey Private key of the RSA key pair that was established with the service provider
|
197
|
+
# @return {String} RSA signature matching the contents of signature base string
|
198
|
+
#
|
199
|
+
# noinspection RubyArgCount
|
200
|
+
def OAuth.sign_signature_base_string(sbs, signing_key)
|
201
|
+
digest = OpenSSL::Digest::SHA256.new
|
202
|
+
rsa_key = OpenSSL::PKey::RSA.new signing_key
|
203
|
+
|
204
|
+
signature = ''
|
205
|
+
begin
|
206
|
+
signature = rsa_key.sign(digest, sbs)
|
207
|
+
rescue
|
208
|
+
raise Exception, 'Unable to sign the signature base string.'
|
209
|
+
end
|
210
|
+
|
211
|
+
Base64.strict_encode64(signature).chomp.gsub(/\n/, '')
|
212
|
+
end
|
213
|
+
|
214
|
+
#
|
215
|
+
# Generates a hash based on request payload as per
|
216
|
+
# https://tools.ietf.org/id/draft-eaton-oauth-bodyhash-00.html
|
217
|
+
#
|
218
|
+
# @param {Any} payload Request payload
|
219
|
+
# @return {String} Base64 encoded cryptographic hash of the given payload
|
220
|
+
#
|
221
|
+
def get_body_hash(payload)
|
222
|
+
# Base 64 encodes the SHA1 digest of payload
|
223
|
+
Base64.strict_encode64(Digest::SHA256.digest(payload.nil? ? '' : payload))
|
224
|
+
end
|
225
|
+
|
226
|
+
#
|
227
|
+
# Encodes a text string as a valid component of a Uniform Resource Identifier (URI).
|
228
|
+
# @ param uri_component A value representing an encoded URI component.
|
229
|
+
#
|
230
|
+
def encode_uri_component(uri_component)
|
231
|
+
URI.encode_www_form_component(uri_component)
|
232
|
+
end
|
233
|
+
|
234
|
+
#
|
235
|
+
# Generates a random string for replay protection as per
|
236
|
+
# https://tools.ietf.org/html/rfc5849#section-3.3
|
237
|
+
# @return {String} UUID with dashes removed
|
238
|
+
#
|
239
|
+
def get_nonce(len = 32)
|
240
|
+
# Returns a random string of length=len
|
241
|
+
o = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten
|
242
|
+
(0...len).map { o[rand(o.length)] }.join
|
243
|
+
end
|
244
|
+
|
245
|
+
# Returns UNIX Timestamp as required per
|
246
|
+
# https://tools.ietf.org/html/rfc5849#section-3.3
|
247
|
+
# @return {String} UNIX timestamp (UTC)
|
248
|
+
#
|
249
|
+
def time_stamp
|
250
|
+
Time.now.getutc.to_i
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.name = "mastercard_oauth1_signer"
|
3
|
+
gem.authors = ["Mastercard"]
|
4
|
+
gem.email = ["APISupport@mastercard.com"]
|
5
|
+
gem.summary = %q{OAuth signature SDK}
|
6
|
+
gem.description = %q{Zero dependency library for generating a Mastercard API compliant OAuth signature}
|
7
|
+
gem.version = "1.0.0"
|
8
|
+
gem.license = "MIT"
|
9
|
+
gem.homepage = "https://github.com/Mastercard/oauth1-signer-ruby"
|
10
|
+
gem.files = Dir["{bin,spec,lib}/**/*"]+ Dir["data/*"] + ["oauth1_signer_ruby.gemspec"]
|
11
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
13
|
+
gem.require_paths = ["lib"]
|
14
|
+
gem.add_development_dependency "bundler", "~> 1.5"
|
15
|
+
gem.add_development_dependency "rake", "~> 0"
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mastercard_oauth1_signer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mastercard
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-01-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Zero dependency library for generating a Mastercard API compliant OAuth
|
42
|
+
signature
|
43
|
+
email:
|
44
|
+
- APISupport@mastercard.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- lib/oauth.rb
|
50
|
+
- oauth1_signer_ruby.gemspec
|
51
|
+
homepage: https://github.com/Mastercard/oauth1-signer-ruby
|
52
|
+
licenses:
|
53
|
+
- MIT
|
54
|
+
metadata: {}
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
requirements: []
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 2.7.7
|
72
|
+
signing_key:
|
73
|
+
specification_version: 4
|
74
|
+
summary: OAuth signature SDK
|
75
|
+
test_files: []
|