mastercard_oauth1_signer 1.1.2 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
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