mastercard_oauth1_signer 1.1.2 → 1.1.3

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