├── .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/_/_/src/global/assets/svg/mcdev-logo-dark.svg)](https://developer.mastercard.com/) 3 | 4 | [![maintenance-status](https://img.shields.io/badge/maintenance-deprecated-red.svg)](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md) 5 | [![](https://github.com/Mastercard/oauth1-signer-ruby/workflows/Build%20&%20Test/badge.svg)](https://github.com/Mastercard/oauth1-signer-ruby/actions?query=workflow%3A%22Build+%26+Test%22) 6 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_oauth1-signer-ruby&metric=alert_status)](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-ruby) 7 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_oauth1-signer-ruby&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-ruby) 8 | [![](https://img.shields.io/gem/v/mastercard_oauth1_signer.svg)](https://rubygems.org/gems/mastercard_oauth1_signer) 9 | [![](https://img.shields.io/badge/license-MIT-yellow.svg)](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 | --------------------------------------------------------------------------------