├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── pull_request_template.md
└── workflows
│ ├── broken-links.yml
│ ├── sonar.yml
│ └── test.yml
├── .gitignore
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── lib
└── mastercard
│ └── oauth.rb
├── oauth1_signer_ruby.gemspec
└── tests
├── test_get_authorization_string.rb
├── test_get_base_uri_string.rb
├── test_get_body_hash.rb
├── test_get_nonce.rb
├── test_get_oauth_params.rb
├── test_get_signature_base_string.rb
├── test_get_time_stamp.rb
├── test_helper.rb
├── test_oauth.rb
├── test_oauth_extract_query_params.rb
└── test_to_oauth_param_string.rb
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a bug report to help us improve
4 | title: "[BUG] Description"
5 | labels: 'Issue: Bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | #### Bug Report Checklist
11 |
12 | - [ ] Have you provided a code sample to reproduce the issue?
13 | - [ ] Have you tested with the latest release to confirm the issue still exists?
14 | - [ ] Have you searched for related issues/PRs?
15 | - [ ] What's the actual output vs expected output?
16 |
17 |
20 |
21 | **Description**
22 | A clear and concise description of what is the question, suggestion, or issue and why this is a problem for you.
23 |
24 | **To Reproduce**
25 | Steps to reproduce the behavior.
26 |
27 | **Expected behavior**
28 | A clear and concise description of what you expected to happen.
29 |
30 | **Screenshots**
31 | If applicable, add screenshots to help explain your problem.
32 |
33 | **Additional context**
34 | Add any other context about the problem here (OS, language version, etc..).
35 |
36 |
37 | **Related issues/PRs**
38 | Has a similar issue/PR been reported/opened before?
39 |
40 | **Suggest a fix/enhancement**
41 | If you can't fix the bug yourself, perhaps you can point to what might be causing the problem (line of code or commit), or simply make a suggestion.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[REQ] Feature Request Description"
5 | labels: 'Enhancement: Feature'
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Is your feature request related to a problem? Please describe.
11 |
12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
13 |
14 | ### Describe the solution you'd like
15 |
16 | A clear and concise description of what you want to happen.
17 |
18 | ### Describe alternatives you've considered
19 |
20 | A clear and concise description of any alternative solutions or features you've considered.
21 |
22 | ### Additional context
23 |
24 | Add any other context or screenshots about the feature request here.
25 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 | ### PR checklist
3 |
4 | - [ ] An issue/feature request has been created for this PR
5 | - [ ] Pull Request title clearly describes the work in the pull request and the Pull Request description provides details about how to validate the work. Missing information here may result in a delayed response.
6 | - [ ] File the PR against the `master` branch
7 | - [ ] The code in this PR is covered by unit tests
8 |
9 | #### Link to issue/feature request: *add the link here*
10 |
11 | #### Description
12 | A clear and concise description of what is this PR for and any additional info might be useful for reviewing it.
13 |
--------------------------------------------------------------------------------
/.github/workflows/broken-links.yml:
--------------------------------------------------------------------------------
1 | 'on':
2 | push:
3 | branches:
4 | - "**"
5 | schedule:
6 | - cron: 0 16 * * *
7 | workflow_dispatch:
8 | name: broken links?
9 | jobs:
10 | linkChecker:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Link Checker
15 | id: lc
16 | uses: peter-evans/link-checker@v1.2.2
17 | with:
18 | args: '-v -r *.md'
19 | - name: Fail?
20 | run: 'exit ${{ steps.lc.outputs.exit_code }}'
21 |
--------------------------------------------------------------------------------
/.github/workflows/sonar.yml:
--------------------------------------------------------------------------------
1 | name: Sonar
2 | 'on':
3 | push:
4 | branches:
5 | - "**"
6 | pull_request_target:
7 | branches:
8 | - "**"
9 | types: [opened, synchronize, reopened, labeled]
10 | schedule:
11 | - cron: 0 16 * * *
12 | workflow_dispatch:
13 | jobs:
14 | test:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 | with:
19 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
20 | - name: Check for external PR
21 | if: ${{ !(contains(github.event.pull_request.labels.*.name, 'safe') ||
22 | github.event.pull_request.head.repo.full_name == github.repository ||
23 | github.event_name != 'pull_request_target') }}
24 | run: echo "Unsecure PR, must be labelled with the 'safe' label, then run the workflow again" && exit 1
25 | - name: Set up Ruby
26 | uses: ruby/setup-ruby@v1
27 | with:
28 | ruby-version: 2.7
29 | - name: Setup java
30 | uses: actions/setup-java@v1
31 | with:
32 | java-version: '11'
33 | - name: Install dependencies
34 | run: |
35 | wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-3.3.0.1492.zip
36 | unzip sonar-scanner-cli-3.3.0.1492.zip
37 | bundle install --jobs=3 --retry=3
38 | - name: Run tests
39 | run: |
40 | gem build *.gemspec
41 | gem install *.gem
42 | rake test
43 | - name: Sonar
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
47 | run: |
48 | sonar-scanner-3.3.0.1492/bin/sonar-scanner \
49 | -Dsonar.projectName=oauth1-signer-ruby \
50 | -Dsonar.projectKey=Mastercard_oauth1-signer-ruby \
51 | -Dsonar.organization=mastercard \
52 | -Dsonar.sources=./lib \
53 | -Dsonar.tests=./tests \
54 | -Dsonar.ruby.coverage.reportPaths=coverage/.resultset.json \
55 | -Dsonar.host.url=https://sonarcloud.io \
56 | -Dsonar.login=$SONAR_TOKEN
57 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Build & Test
2 | 'on':
3 | push:
4 | branches:
5 | - "**"
6 | pull_request:
7 | branches:
8 | - "**"
9 | schedule:
10 | - cron: 0 16 * * *
11 | workflow_dispatch:
12 | jobs:
13 | test:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | rvm:
18 | - truffleruby
19 | - 2.5
20 | - 2.6
21 | - 2.7
22 | - 3.0
23 | steps:
24 | - uses: actions/checkout@v2
25 | - name: Set up Ruby
26 | uses: ruby/setup-ruby@v1
27 | with:
28 | ruby-version: '${{ matrix.rvm }}'
29 | - name: Install dependencies
30 | run: bundle install --jobs=3 --retry=3
31 | - name: Run tests
32 | run: |
33 | gem build *.gemspec
34 | gem install *.gem
35 | rake test
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #simplecov
2 | coverage
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gemspec
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 - 2021 Mastercard
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # oauth1-signer-ruby
2 | [](https://developer.mastercard.com/)
3 |
4 | [](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md)
5 | [](https://github.com/Mastercard/oauth1-signer-ruby/actions?query=workflow%3A%22Build+%26+Test%22)
6 | [](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-ruby)
7 | [](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-ruby)
8 | [](https://rubygems.org/gems/mastercard_oauth1_signer)
9 | [](https://github.com/Mastercard/oauth1-signer-ruby/blob/master/LICENSE)
10 |
11 |
12 | ## Table of Contents
13 | - [Overview](#overview)
14 | * [Compatibility](#compatibility)
15 | * [References](#references)
16 | * [Versioning and Deprecation Policy](#versioning)
17 | - [Usage](#usage)
18 | * [Prerequisites](#prerequisites)
19 | * [Adding the Library to Your Project](#adding-the-library-to-your-project)
20 | * [Loading the Signing Key](#loading-the-signing-key)
21 | * [Creating the OAuth Authorization Header](#creating-the-oauth-authorization-header)
22 | * [Integrating with OpenAPI Generator API Client Libraries](#integrating-with-openapi-generator-api-client-libraries)
23 |
24 | ## Overview
25 | Zero dependency library for generating a Mastercard API compliant OAuth signature.
26 |
27 | ### Compatibility
28 | * Ruby 2.5+
29 | * Truffle Ruby 1.0.0+
30 |
31 | ### References
32 | * [OAuth 1.0a specification](https://tools.ietf.org/html/rfc5849)
33 | * [Body hash extension for non application/x-www-form-urlencoded payloads](https://tools.ietf.org/id/draft-eaton-oauth-bodyhash-00.html)
34 |
35 | ### Versioning and Deprecation Policy
36 | * [Mastercard Versioning and Deprecation Policy](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md)
37 |
38 | ## Usage
39 | ### Prerequisites
40 | Before using this library, you will need to set up a project in the [Mastercard Developers Portal](https://developer.mastercard.com).
41 |
42 | As part of this set up, you'll receive credentials for your app:
43 | * A consumer key (displayed on the Mastercard Developer Portal)
44 | * A private request signing key (matching the public certificate displayed on the Mastercard Developer Portal)
45 |
46 | ### Adding the Library to Your Project
47 |
48 | ```shell
49 | gem install mastercard_oauth1_signer
50 | ```
51 |
52 | ### Loading the Signing Key
53 |
54 | The following code shows how to load the private key using `OpenSSL`:
55 | ```ruby
56 | require 'openssl'
57 |
58 | is = File.binread("");
59 | signing_key = OpenSSL::PKCS12.new(is, "").key;
60 | ```
61 |
62 | ### Creating the OAuth Authorization Header
63 | The method that does all the heavy lifting is `Mastercard::OAuth.get_authorization_header`. You can call into it directly and as long as you provide the correct parameters, it will return a string that you can add into your request's `Authorization` header.
64 |
65 | ```ruby
66 | require 'mastercard/oauth'
67 |
68 | consumer_key = "";
69 | uri = "https://sandbox.api.mastercard.com/service";
70 | method = "POST";
71 | payload = "Hello world!";
72 | authHeader = Mastercard::OAuth.get_authorization_header(uri, method, payload, consumer_key, signing_key);
73 | ```
74 |
75 | ### Integrating with OpenAPI Generator API Client Libraries
76 |
77 | [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) generates API client libraries from [OpenAPI Specs](https://github.com/OAI/OpenAPI-Specification).
78 | It provides generators and library templates for supporting multiple languages and frameworks.
79 |
80 | Generators currently supported:
81 | + [ruby](#ruby)
82 |
83 | #### ruby
84 |
85 | ##### OpenAPI Generator
86 |
87 | Client libraries can be generated using the following command:
88 | ```shell
89 | openapi-generator-cli generate -i openapi-spec.yaml -g ruby -o out
90 | ```
91 | See also:
92 | * [OpenAPI Generator CLI Installation](https://openapi-generator.tech/docs/installation/)
93 | * [CONFIG OPTIONS for ruby](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/ruby.md)
94 |
95 | ##### Callback method `Typhoeus.before`
96 |
97 | The Authorization header can be hooked into before a request run:
98 |
99 | ```ruby
100 | config = OpenapiClient::Configuration.default
101 | api_client = OpenapiClient::ApiClient.new
102 | config.basePath = "https://sandbox.api.mastercard.com"
103 | api_client.config = config
104 |
105 | Typhoeus.before { |request|
106 | authHeader =
107 | Mastercard::OAuth.get_authorization_header request.url, request.options[:method],
108 | request.options[:body], consumer_key, signing_key.key
109 | request.options[:headers] = request.options[:headers].merge({'Authorization' => authHeader})
110 | }
111 |
112 | serviceApi = service.ServiceApi.new api_client
113 |
114 | opts = {}
115 | serviceApi.call opts
116 | // …
117 | ```
118 |
119 | See also: https://rubydoc.info/github/typhoeus/typhoeus/frames/Typhoeus#before-class_method
120 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake'
3 | require 'rake/clean'
4 | require 'rake/testtask'
5 |
6 | desc "Run tests"
7 | task default: 'test'
8 |
9 | Rake::TestTask.new do |t|
10 | t.libs << 'tests'
11 | t.test_files = FileList['tests/test_*.rb']
12 | # Load SimpleCov before starting the tests
13 | t.ruby_opts = ['-r "./tests/test_helper"']
14 | t.verbose = true
15 | end
16 |
17 | Dir['tasks/**/*.rake'].each { |t| load t }
18 |
--------------------------------------------------------------------------------
/lib/mastercard/oauth.rb:
--------------------------------------------------------------------------------
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} 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} 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>} queryParamsMap Map of all oauth parameters that need to be signed
130 | # @param {Map} 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
256 |
--------------------------------------------------------------------------------
/oauth1_signer_ruby.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |gem|
2 | gem.name = "mastercard_oauth1_signer"
3 | gem.authors = ["Mastercard"]
4 | gem.summary = %q{Mastercard client authentication library}
5 | gem.description = %q{Zero dependency library for generating a Mastercard API compliant OAuth signature}
6 | gem.version = "1.1.4"
7 | gem.license = "MIT"
8 | gem.homepage = "https://github.com/Mastercard/oauth1-signer-ruby"
9 | gem.files = Dir["{bin,spec,lib}/**/*"]+ Dir["data/*"] + ["oauth1_signer_ruby.gemspec"]
10 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
11 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
12 | gem.require_paths = ["lib"]
13 |
14 | gem.add_development_dependency 'bundler', '>= 1.5'
15 | gem.add_development_dependency 'minitest', '~> 5.0'
16 | gem.add_development_dependency 'rake', ">= 12.3.3"
17 | gem.add_development_dependency 'simplecov', '~> 0.16.1'
18 | end
19 |
--------------------------------------------------------------------------------
/tests/test_get_authorization_string.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'minitest/mock'
3 | require_relative '../lib/mastercard/oauth'
4 |
5 |
6 | class TestGetAuthorizationString < Minitest::Test
7 |
8 | # Creates a valid OAuth1.0a signature with a body hash when payload is present
9 | def test_get_authorization_string
10 | consumer_key = 'aaa!aaa'
11 |
12 | Mastercard::OAuth.stub(:get_nonce, "uTeLPs6K") do
13 | Mastercard::OAuth.stub(:time_stamp, "1524771555") do
14 | oauth_params = Mastercard::OAuth.get_oauth_params consumer_key
15 | authorization_string = Mastercard::OAuth.get_authorization_string oauth_params
16 | assert_equal authorization_string, 'OAuth oauth_body_hash="47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",oauth_consumer_key="aaa!aaa",oauth_nonce="uTeLPs6K",oauth_signature_method="RSA-SHA256",oauth_timestamp="1524771555",oauth_version="1.0"'
17 | end
18 | end
19 | end
20 | end
--------------------------------------------------------------------------------
/tests/test_get_base_uri_string.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'minitest/mock'
3 | require_relative '../lib/mastercard/oauth'
4 |
5 |
6 | class TestGetBaseUriString < Minitest::Test
7 |
8 | def test_creates_a_normalized_url
9 | href = 'https://sandbox.api.mastercard.com/merchantid/v1/merchantid?MerchantId=GOOGLE%20LTD%20ADWORDS%20%28CC%40GOOGLE.COM%29&Format=XML&Type=ExactMatch&Format=JSON'
10 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
11 | assert_equal('https://sandbox.api.mastercard.com/merchantid/v1/merchantid', base_uri)
12 | end
13 |
14 | def test_creates_a_normalized_url_with_uppercase_path
15 | href = 'https://sandbox.api.mastercard.com/merchantid/v1/getMerchantId?MerchantId=GOOGLE%20LTD%20ADWORDS%20%28CC%40GOOGLE.COM%29&Format=XML&Type=ExactMatch&Format=JSON'
16 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
17 | assert_equal('https://sandbox.api.mastercard.com/merchantid/v1/getMerchantId', base_uri)
18 | end
19 |
20 | def test_supports_rfc_examples
21 | href = 'https://www.example.net:8080'
22 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
23 | assert_equal('https://www.example.net:8080/', base_uri)
24 |
25 | href = 'http://EXAMPLE.COM:80/r%20v/X?id=123'
26 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
27 | assert_equal('http://example.com/r%20v/X', base_uri)
28 | end
29 |
30 | def test_removes_redundant_ports
31 | href = 'https://api.mastercard.com:443/test?query=param'
32 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
33 | assert_equal('https://api.mastercard.com/test', base_uri)
34 |
35 | href = 'http://api.mastercard.com:80/test'
36 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
37 | assert_equal('http://api.mastercard.com/test', base_uri)
38 |
39 | href = 'https://api.mastercard.com:17443/test?query=param'
40 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
41 | assert_equal('https://api.mastercard.com:17443/test', base_uri)
42 |
43 | href = 'http://api.mastercard.com:1780/test?query=param'
44 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
45 | assert_equal('http://api.mastercard.com:1780/test', base_uri)
46 | end
47 |
48 | def test_removes_fragments
49 | href = 'https://api.mastercard.com/test?query=param#fragment'
50 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
51 | assert_equal('https://api.mastercard.com/test', base_uri)
52 | end
53 |
54 | def test_adds_trailing_slash
55 | href = 'https://api.mastercard.com'
56 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
57 | assert_equal('https://api.mastercard.com/', base_uri)
58 |
59 | href = 'https://api.mastercard.com/'
60 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
61 | assert_equal('https://api.mastercard.com/', base_uri)
62 | end
63 |
64 | def test_uses_lowercase_scheme_and_host
65 | href = 'HTTPS://API.MASTERCARD.COM/TEST'
66 | base_uri = Mastercard::OAuth.get_base_uri_string(href)
67 | assert_equal('https://api.mastercard.com/TEST', base_uri)
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/tests/test_get_body_hash.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'minitest/mock'
3 | require_relative '../lib/mastercard/oauth'
4 |
5 | class TestGetBodyHash < Minitest::Test
6 |
7 | def test_creates_base64_encoded_cryptographic_hash_of_the_given_payload
8 | my_payload = '{ my: "payload" }'
9 | body_hash = Mastercard::OAuth.get_body_hash my_payload
10 |
11 | assert_equal body_hash, 'Qm/nLCqwlog0uoCDvypgninzNQ25YHgTmUDl/zOgT1s='
12 | end
13 | end
--------------------------------------------------------------------------------
/tests/test_get_nonce.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'minitest/mock'
3 | require_relative '../lib/mastercard/oauth'
4 |
5 | class TestGetNonce < Minitest::Test
6 |
7 | NONCE_LENGTH = 32
8 | VALID_CHARS = Regexp.new('^[a-zA-Z0-9_]*$').freeze
9 |
10 | def test_creates_UUID_with_dashes_removed
11 |
12 | nonce = Mastercard::OAuth.get_nonce
13 |
14 | assert_equal(nonce.length, NONCE_LENGTH)
15 | assert_equal(VALID_CHARS.match?(nonce), true)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/tests/test_get_oauth_params.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'minitest/mock'
3 | require_relative '../lib/mastercard/oauth'
4 |
5 | class TestGetOAuthParams < Minitest::Test
6 |
7 | CONSUMER_KEY = 'aaa!aaa'.freeze
8 | MY_PAYLOAD = '{ my: "payload" }'.freeze
9 |
10 | def test_creates_map_with_ordered_oauth_parameters
11 | oauth_params = Mastercard::OAuth.get_oauth_params(CONSUMER_KEY)
12 | map_keys = oauth_params.keys
13 | params = %w[oauth_body_hash oauth_consumer_key oauth_nonce oauth_signature_method oauth_timestamp oauth_version]
14 | assert_equal(map_keys, params)
15 | end
16 |
17 | def test_creates_map_with_ordered_oauth_parameters_with_payload
18 | oauth_params = Mastercard::OAuth.get_oauth_params(CONSUMER_KEY, MY_PAYLOAD)
19 | map_keys = oauth_params.keys
20 | params = %w[oauth_body_hash oauth_consumer_key oauth_nonce oauth_signature_method oauth_timestamp oauth_version]
21 | assert_equal(map_keys, params)
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/tests/test_get_signature_base_string.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'minitest/mock'
3 | require_relative '../lib/mastercard/oauth'
4 |
5 | class TestGetSignatureBaseString < Minitest::Test
6 |
7 | def test_creates_a_correctly_constructed_and_escaped_signature_base_string
8 |
9 | http_method = "GET"
10 | base_uri = "https://sandbox.api.mastercard.com/merchantid/v1/merchantid"
11 | param_string = "Format=JSON&Format=XML&MerchantId=GOOGLE%20LTD%20ADWORDS%20%28CC%40GOOGLE.COM%29&Type=ExactMatch&oauth_consumer_key=aaa!aaa&oauth_nonce=uTeLPs6K&oauth_signature_method=RSA-SHA256&oauth_timestamp=1524771555&oauth_version=1.0"
12 | sbs = Mastercard::OAuth.get_signature_base_string(http_method, base_uri, param_string)
13 |
14 | assert_equal(sbs, "GET&https%3A%2F%2Fsandbox.api.mastercard.com%2Fmerchantid%2Fv1%2Fmerchantid&Format%3DJSON%26Format%3DXML%26MerchantId%3DGOOGLE%2520LTD%2520ADWORDS%2520%2528CC%2540GOOGLE.COM%2529%26Type%3DExactMatch%26oauth_consumer_key%3Daaa%21aaa%26oauth_nonce%3DuTeLPs6K%26oauth_signature_method%3DRSA-SHA256%26oauth_timestamp%3D1524771555%26oauth_version%3D1.0");
15 | end
16 |
17 | def test_creates_a_correctly_constructed_and_escaped_signature_base_string_given_encoded_params
18 |
19 | http_method = "GET"
20 | consumer_key = "aaa!aaa"
21 |
22 | query_params = Mastercard::OAuth.extract_query_params'https://example.com/?param=token1%3Atoken2'
23 | oauth_params = Mastercard::OAuth.get_oauth_params consumer_key
24 | param_string = Mastercard::OAuth.to_oauth_param_string query_params, oauth_params
25 |
26 | sbs = Mastercard::OAuth.get_signature_base_string(http_method, "https://example.com", param_string)
27 |
28 | assert(sbs.include? "oauth_version%3D1.0%26param%3Dtoken1%253Atoken2");
29 | end
30 |
31 | def test_creates_a_correctly_constructed_and_escaped_signature_base_string_given_decoded_params
32 |
33 | http_method = "GET"
34 | consumer_key = "aaa!aaa"
35 |
36 | query_params = Mastercard::OAuth.extract_query_params'https://example.com/?param=token1:token2'
37 | oauth_params = Mastercard::OAuth.get_oauth_params consumer_key
38 | param_string = Mastercard::OAuth.to_oauth_param_string query_params, oauth_params
39 |
40 | sbs = Mastercard::OAuth.get_signature_base_string(http_method, "https://example.com", param_string)
41 |
42 | assert(sbs.include? "oauth_version%3D1.0%26param%3Dtoken1%3Atoken2");
43 | end
44 |
45 | end
46 |
--------------------------------------------------------------------------------
/tests/test_get_time_stamp.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'minitest/mock'
3 | require_relative '../lib/mastercard/oauth'
4 |
5 | class TestGetTimeStamp < Minitest::Test
6 |
7 | def test_creates_UNIX_timestamp_UTC
8 | timestamp = Mastercard::OAuth.time_stamp
9 | assert_equal(timestamp > 0, true)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/tests/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'simplecov'
2 | SimpleCov.start
3 | require 'minitest/autorun'
--------------------------------------------------------------------------------
/tests/test_oauth.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'minitest/mock'
3 | require_relative '../lib/mastercard/oauth'
4 |
5 | class Mastercard::OAuthTest < Minitest::Test
6 |
7 | # Fake tests
8 | # Creates a valid OAuth1.0a signature with a body hash when payload is not present
9 | def test_get_authorization_header
10 | uri = "HTTPS://SANDBOX.api.mastercard.com/merchantid/v1/merchantid?MerchantId=GOOGLE%20LTD%20ADWORDS%20%28CC%40GOOGLE.COM%29&Type=ExactMatch&Format=JSON"
11 | method = "GET"
12 | consumer_key = "aaa!aaa"
13 | signing_key = "XXX"
14 |
15 | Mastercard::OAuth.stub(:get_nonce, "uTeLPs6K") do
16 | Mastercard::OAuth.stub(:sign_signature_base_string, "XXX") do
17 | Mastercard::OAuth.stub(:time_stamp, "1524771555") do
18 | header = Mastercard::OAuth.get_authorization_header uri, method, nil, consumer_key, signing_key
19 | assert_equal header, 'OAuth oauth_body_hash="47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",oauth_consumer_key="aaa!aaa",oauth_nonce="uTeLPs6K",oauth_signature_method="RSA-SHA256",oauth_timestamp="1524771555",oauth_version="1.0",oauth_signature="XXX"'
20 | end
21 | end
22 | end
23 | end
24 | end
--------------------------------------------------------------------------------
/tests/test_oauth_extract_query_params.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require_relative '../lib/mastercard/oauth'
3 |
4 | class TestOAuthExtractQueryParams < Minitest::Test
5 |
6 | @test_href = ''
7 | @query_params = ''
8 |
9 | def setup
10 | @test_href = "HTTPS://SANDBOX.api.mastercard.com/merchantid/v1/merchantid?MerchantId=GOOGLE%20LTD%20ADWORDS%20%28CC%40GOOGLE.COM%29&Format=XML&Type=ExactMatch&Format=JSON"
11 | @query_params = Mastercard::OAuth.extract_query_params(@test_href)
12 | end
13 |
14 | def test_should_return_a_map
15 | assert_equal true, (@query_params.instance_of? Hash)
16 | end
17 |
18 | def test_query_parameter_keys_should_be_sorted
19 | map_keys_array = @query_params.keys
20 | params = %w[Format MerchantId Type]
21 | assert_equal map_keys_array, params
22 | end
23 |
24 | def test_query_parameter_values_should_be_sorted_Values_for_parameters_with_the_same_name_are_added_into_a_list
25 | # Format
26 | values_format = @query_params['Format']
27 | assert_equal true, values_format.instance_of?(Array)
28 | assert_equal(values_format.size, 2)
29 | assert_equal values_format, %w[JSON XML]
30 |
31 | # MerchantId
32 | values_merchant_id = @query_params['MerchantId']
33 | assert_equal true, values_format.instance_of?(Array)
34 | assert_equal(values_merchant_id.size, 1)
35 | assert_equal values_merchant_id, ['GOOGLE%20LTD%20ADWORDS%20%28CC%40GOOGLE.COM%29']
36 |
37 | # Type
38 | values_type = @query_params['Type']
39 | assert_equal(values_type.size, 1)
40 | assert_equal true, values_format.instance_of?(Array)
41 | assert_equal values_type, ['ExactMatch']
42 | end
43 |
44 | def test_extract_query_params_should_support_rfc_example_when_uri_created_from_uri_string
45 | href = 'https://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b'
46 | params = Mastercard::OAuth.extract_query_params(href)
47 |
48 | assert_equal params['b5'], ['%3D%253D']
49 | assert_equal params['a3'], ['a']
50 | assert_equal params['c%40'], ['']
51 | assert_equal params['a2'], ['r%20b']
52 | end
53 |
54 | def test_extract_query_params_should_support_rfc_example_when_uri_created_from_components
55 | uri = URI::HTTPS.build(host: 'example.com', query: 'b5=%3D%253D&a3=a&c%40=&a2=r%20b').to_s
56 | params = Mastercard::OAuth.extract_query_params(uri)
57 |
58 | assert_equal params['b5'], ['%3D%253D']
59 | assert_equal params['a3'], ['a']
60 | assert_equal params['c%40'], ['']
61 | assert_equal params['a2'], ['r%20b']
62 | end
63 |
64 | def test_extract_query_params_should_not_encode_params_when_uri_created_from_string_with_decoded_params
65 | href = 'https://example.com/request?colon=:&plus=+&comma=,'
66 | params = Mastercard::OAuth.extract_query_params(href)
67 |
68 | assert_equal params['colon'], [':']
69 | assert_equal params['plus'], ['+']
70 | assert_equal params['comma'], [',']
71 | end
72 | end
--------------------------------------------------------------------------------
/tests/test_to_oauth_param_string.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/autorun'
4 | require 'minitest/mock'
5 | require_relative '../lib/mastercard/oauth'
6 |
7 | class TestToOAuthParamString < Minitest::Test
8 | def test_creates_a_correctly_encoded_and_sorted_OAuth_parameter_string
9 | consumer_key = 'aaa!aaa'
10 | Mastercard::OAuth.stub(:get_nonce, 'uTeLPs6K') do
11 | Mastercard::OAuth.stub(:time_stamp, '1524771555') do
12 | query_params = Mastercard::OAuth.extract_query_params 'HTTPS://SANDBOX.api.mastercard.com/merchantid/v1/merchantid?MerchantId=GOOGLE%20LTD%20ADWORDS%20%28CC%40GOOGLE.COM%29&Format=XML&Type=ExactMatch&Format=JSON'
13 | oauth_params = Mastercard::OAuth.get_oauth_params consumer_key
14 | param_string = Mastercard::OAuth.to_oauth_param_string query_params, oauth_params
15 | assert_equal param_string, 'Format=JSON&Format=XML&MerchantId=GOOGLE%20LTD%20ADWORDS%20%28CC%40GOOGLE.COM%29&Type=ExactMatch&oauth_body_hash=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=&oauth_consumer_key=aaa!aaa&oauth_nonce=uTeLPs6K&oauth_signature_method=RSA-SHA256&oauth_timestamp=1524771555&oauth_version=1.0'
16 | end
17 | end
18 | end
19 |
20 | def test_should_use_ascending_byte_value_ordering
21 | query_params = { 'b' => ['b'], 'A' => %w[a A], 'B' => ['B'], 'a' => %w[A a], '0' => ['0'] }
22 | oauth_params = {}
23 | param_string = Mastercard::OAuth.to_oauth_param_string query_params, oauth_params
24 | assert_equal '0=0&A=A&A=a&B=B&a=A&a=a&b=b', param_string
25 |
26 | # https://oauth.net/core/1.0a/#anchor13
27 | query_params = { 'a' => ['1'], 'c' => ['hi%20there'], 'f' => %w[25 50 a], 'z' => %w[p t] }
28 | oauth_params = {}
29 | param_string = Mastercard::OAuth.to_oauth_param_string query_params, oauth_params
30 | assert_equal 'a=1&c=hi%20there&f=25&f=50&f=a&z=p&z=t', param_string
31 | end
32 | end
33 |
--------------------------------------------------------------------------------