├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── broken-links.yml │ ├── lint.yml │ ├── sonar.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── oauth1-signer-python.pyproj ├── oauth1-signer-python.sln ├── oauth1 ├── __init__.py ├── authenticationutils.py ├── coreutils.py ├── oauth.py ├── oauth_ext.py ├── signer.py ├── signer_interceptor.py └── version.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── sonar-project.properties ├── test_key_container.p12 └── tests ├── __init__.py ├── test_interceptor.py ├── test_oauth.py ├── test_oauth_ext.py ├── test_signer.py └── test_utils.py /.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. 42 | -------------------------------------------------------------------------------- /.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 | name: broken links? 2 | 'on': 3 | push: 4 | branches: 5 | - "**" 6 | schedule: 7 | - cron: 0 16 * * * 8 | workflow_dispatch: 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/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linter 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 | sonarcloud: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Python Style Checker 19 | uses: andymckay/pycodestyle-action@0.1.3 -------------------------------------------------------------------------------- /.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 | sonarcloud: 15 | name: Sonar 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - name: Check for external PR 22 | if: ${{ !(contains(github.event.pull_request.labels.*.name, 'safe') || 23 | github.event.pull_request.head.repo.full_name == github.repository || 24 | github.event_name != 'pull_request_target') }} 25 | run: echo "Unsecure PR, must be labelled with the 'safe' label, then run the workflow again" && exit 1 26 | - name: Set up Python 3.8 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: 3.8 30 | - name: Install dependencies 31 | run: | 32 | pip3 install -r requirements.txt 33 | pip3 install . 34 | pip3 install coverage 35 | - name: Run Tests 36 | run: | 37 | coverage run setup.py test 38 | coverage xml 39 | - name: SonarCloud Scan 40 | uses: SonarSource/sonarcloud-github-action@master 41 | env: 42 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 43 | SONAR_TOKEN: '${{ secrets.SONAR_TOKEN }}' 44 | -------------------------------------------------------------------------------- /.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 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | python-version: 18 | - 3.8 19 | - 3.9 20 | include: 21 | - os: "ubuntu-latest" 22 | steps: 23 | - uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 0 26 | - name: 'Set up Python ${{ matrix.python-version }}' 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: '${{ matrix.python-version }}' 30 | - name: Install dependencies 31 | run: | 32 | pip3 install -r requirements.txt 33 | pip3 install . 34 | pip3 install coverage 35 | - name: Run Tests 36 | run: | 37 | coverage run setup.py test 38 | coverage xml 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | *.iml 107 | .idea 108 | 109 | # Visual Studio 110 | .vs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 - 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-python 2 | [![](https://developer.mastercard.com/_/_/src/global/assets/svg/mcdev-logo-dark.svg)](https://developer.mastercard.com/) 3 | 4 | [![](https://github.com/Mastercard/oauth1-signer-python/workflows/Build%20&%20Test/badge.svg)](https://github.com/Mastercard/oauth1-signer-python/actions?query=workflow%3A%22Build+%26+Test%22) 5 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_oauth1-signer-python&metric=alert_status)](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-python) 6 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_oauth1-signer-python&metric=coverage)](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-python) 7 | [![](https://sonarcloud.io/api/project_badges/measure?project=Mastercard_oauth1-signer-python&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-python) 8 | [![](https://github.com/Mastercard/oauth1-signer-python/workflows/broken%20links%3F/badge.svg)](https://github.com/Mastercard/oauth1-signer-python/actions?query=workflow%3A%22broken+links%3F%22) 9 | [![](https://img.shields.io/pypi/v/mastercard-oauth1-signer.svg?style=flat&color=blue)](https://pypi.org/project/mastercard-oauth1-signer) 10 | [![](https://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/Mastercard/oauth1-signer-python/blob/master/LICENSE) 11 | 12 | 13 | ## Table of Contents 14 | - [Overview](#overview) 15 | * [Compatibility](#compatibility) 16 | * [References](#references) 17 | * [Versioning and Deprecation Policy](#versioning) 18 | - [Usage](#usage) 19 | * [Prerequisites](#prerequisites) 20 | * [Adding the Library to Your Project](#adding-the-library-to-your-project) 21 | * [Importing the Code](#importing-the-code) 22 | * [Loading the Signing Key](#loading-the-signing-key) 23 | * [Creating the OAuth Authorization Header](#creating-the-oauth-authorization-header) 24 | * [Signing HTTP Client Request Objects](#signing-http-client-request-objects) 25 | * [Integrating with OpenAPI Generator API Client Libraries](#integrating-with-openapi-generator-api-client-libraries) 26 | 27 | ## Overview 28 | Python library for generating a Mastercard API compliant OAuth signature. 29 | 30 | ### Compatibility 31 | Python 3.8+ 32 | 33 | ### References 34 | * [OAuth 1.0a specification](https://tools.ietf.org/html/rfc5849) 35 | * [Body hash extension for non application/x-www-form-urlencoded payloads](https://tools.ietf.org/id/draft-eaton-oauth-bodyhash-00.html) 36 | 37 | ### Versioning and Deprecation Policy 38 | * [Mastercard Versioning and Deprecation Policy](https://github.com/Mastercard/.github/blob/main/CLIENT_LIBRARY_DEPRECATION_POLICY.md) 39 | 40 | ## Usage 41 | ### Prerequisites 42 | Before using this library, you will need to set up a project in the [Mastercard Developers Portal](https://developer.mastercard.com). 43 | 44 | As part of this set up, you'll receive credentials for your app: 45 | * A consumer key (displayed on the Mastercard Developer Portal) 46 | * A private request signing key (matching the public certificate displayed on the Mastercard Developer Portal) 47 | 48 | ### Adding the Library to Your Project 49 | 50 | ``` 51 | pip install mastercard-oauth1-signer 52 | ``` 53 | ### Importing the Code 54 | 55 | ``` python 56 | import oauth1.authenticationutils as authenticationutils 57 | from oauth1.oauth import OAuth 58 | ``` 59 | ### Loading the Signing Key 60 | 61 | A private key object can be created by calling the `authenticationutils.load_signing_key` method: 62 | ``` python 63 | signing_key = authenticationutils.load_signing_key('', '') 64 | ``` 65 | 66 | ### Creating the OAuth Authorization Header 67 | The method that does all the heavy lifting is `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. 68 | 69 | #### POST example 70 | 71 | ```python 72 | uri = 'https://sandbox.api.mastercard.com/service' 73 | payload = 'Hello world!' 74 | authHeader = OAuth.get_authorization_header(uri, 'POST', payload, '', signing_key) 75 | ``` 76 | 77 | #### GET example 78 | ```python 79 | uri = 'https://sandbox.api.mastercard.com/service' 80 | authHeader = OAuth.get_authorization_header(uri, 'GET', None, '', signing_key) 81 | ``` 82 | 83 | #### Use of authHeader with requests module (POST and GET example) 84 | ```python 85 | headerdict = {'Authorization' : authHeader} 86 | requests.post(uri, headers=headerdict, data=payload) 87 | requests.get(uri, headers=headerdict) 88 | ``` 89 | 90 | ### Signing HTTP Client Request Objects 91 | 92 | Alternatively, you can use helper classes for some of the commonly used HTTP clients. 93 | 94 | These classes will modify the provided request object in-place and will add the correct `Authorization` header. Once instantiated with a consumer key and private key, these objects can be reused. 95 | 96 | Usage briefly described below, but you can also refer to the test project for examples. 97 | 98 | + [Requests: HTTP for Humans™](#requests) 99 | 100 | #### Requests: HTTP for Humans™ 101 | 102 | You can sign [request](https://requests.readthedocs.io/en/latest/user/quickstart#make-a-request) objects using the `OAuthSigner` class. 103 | 104 | Usage: 105 | ```python 106 | uri = "https://sandbox.api.mastercard.com/service" 107 | request = Request() 108 | request.method = "POST" 109 | # … 110 | 111 | signer = OAuthSigner(consumer_key, signing_key) 112 | request = signer.sign_request(uri, request) 113 | ``` 114 | 115 | 116 | #### Usage of the `oauth_ext` 117 | The requests library supports custom authentication extensions, with which the procedure of creating and calling such requests can simplify the process of request signing. Please, see the examples below: 118 | 119 | ###### POST example 120 | 121 | ```python 122 | from oauth1.oauth_ext import OAuth1RSA 123 | from oauth1.oauth_ext import HASH_SHA256 124 | import requests 125 | 126 | uri = 'https://sandbox.api.mastercard.com/service' 127 | oauth = OAuth1RSA(consumer_key, signing_key) 128 | header = {'Content-type' : 'application/json', 'Accept' : 'application/json'} 129 | 130 | # Passing payload for data parameter as string 131 | payload = '{"key" : "value"}' 132 | response = requests.post(uri, data=payload, auth=oauth, headers=header) 133 | 134 | # Passing payload for data parameter as Json object 135 | payload = {'key' : 'value'} 136 | response = requests.post(uri, data=json.dumps(payload), auth=oauth, headers=header) 137 | 138 | # Passing payload for json parameter Json object 139 | payload = {'key' : 'value'} 140 | response = requests.post(uri, json=payload, auth=oauth, headers=header) 141 | ``` 142 | 143 | ###### GET example 144 | 145 | ```python 146 | from oauth1.oauth_ext import OAuth1RSA 147 | import requests 148 | 149 | uri = 'https://sandbox.api.mastercard.com/service' 150 | oauth = OAuth1RSA(consumer_key, signing_key) 151 | 152 | # Operation for get call 153 | response = requests.get(uri, auth=oauth) 154 | ``` 155 | 156 | ### Integrating with OpenAPI Generator API Client Libraries 157 | 158 | [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) generates API client libraries from [OpenAPI Specs](https://github.com/OAI/OpenAPI-Specification). 159 | It provides generators and library templates for supporting multiple languages and frameworks. 160 | 161 | This project provides you with classes you can use when configuring your API client. These classes will take care of adding the correct `Authorization` header before sending the request. 162 | 163 | Generators currently supported: 164 | + [python](#python) 165 | 166 | #### python 167 | 168 | ##### OpenAPI Generator 169 | 170 | Client libraries can be generated using the following command: 171 | ```shell 172 | openapi-generator-cli generate -i openapi-spec.yaml -g python -o out 173 | ``` 174 | See also: 175 | * [OpenAPI Generator CLI Installation](https://openapi-generator.tech/docs/installation/) 176 | * [CONFIG OPTIONS for python](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/python.md) 177 | 178 | ##### Usage of the `oauth1.signer_interceptor` 179 | 180 | ```python 181 | import openapi_client 182 | from oauth1.signer_interceptor import add_signer_layer 183 | 184 | # … 185 | config = openapi_client.Configuration() 186 | config.host = 'https://sandbox.api.mastercard.com' 187 | client = openapi_client.ApiClient(config) 188 | add_signer_layer(client, '', '', '') 189 | some_api = openapi_client.SomeApi(client) 190 | result = some_api.do_something() 191 | # … 192 | ``` 193 | -------------------------------------------------------------------------------- /oauth1-signer-python.pyproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | 2.0 6 | {fa855547-dee5-41b1-b0a9-d334718ac7ca} 7 | 8 | setup.py 9 | 10 | . 11 | . 12 | {888888a0-9f3d-457c-b088-3a5042f75d52} 13 | Standard Python launcher 14 | Global|PythonCore|3.7-32 15 | 16 | 17 | 18 | 19 | 10.0 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /oauth1-signer-python.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29009.5 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "oauth1-signer-python", "oauth1-signer-python.pyproj", "{FA855547-DEE5-41B1-B0A9-D334718AC7CA}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {FA855547-DEE5-41B1-B0A9-D334718AC7CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {FA855547-DEE5-41B1-B0A9-D334718AC7CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ExtensibilityGlobals) = postSolution 21 | SolutionGuid = {F8D7A140-4ACA-46F7-BD48-47881C7F0F40} 22 | EndGlobalSection 23 | EndGlobal 24 | -------------------------------------------------------------------------------- /oauth1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/oauth1-signer-python/c084f2e42fdf0aa648321bf137dc26de441f21f4/oauth1/__init__.py -------------------------------------------------------------------------------- /oauth1/authenticationutils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2019-2021 Mastercard 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are 7 | # permitted provided that the following conditions are met: 8 | # 9 | # Redistributions of source code must retain the above copyright notice, this list of 10 | # conditions and the following disclaimer. 11 | # Redistributions in binary form must reproduce the above copyright notice, this list of 12 | # conditions and the following disclaimer in the documentation and/or other materials 13 | # provided with the distribution. 14 | # Neither the name of the MasterCard International Incorporated nor the names of its 15 | # contributors may be used to endorse or promote products derived from this software 16 | # without specific prior written permission. 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 18 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 20 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 22 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 23 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 24 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 25 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | # SUCH DAMAGE. 27 | # 28 | 29 | from cryptography.hazmat.primitives.serialization import pkcs12 30 | 31 | 32 | def load_signing_key(pkcs12_filename, password): 33 | key_content = open(pkcs12_filename, 'rb') 34 | private_key = key_content.read() 35 | key_content.close() 36 | key, certs, addcerts = pkcs12.load_key_and_certificates(private_key, password.encode("utf-8")) 37 | return key 38 | -------------------------------------------------------------------------------- /oauth1/coreutils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2019-2021 Mastercard 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are 7 | # permitted provided that the following conditions are met: 8 | # 9 | # Redistributions of source code must retain the above copyright notice, this list of 10 | # conditions and the following disclaimer. 11 | # Redistributions in binary form must reproduce the above copyright notice, this list of 12 | # conditions and the following disclaimer in the documentation and/or other materials 13 | # provided with the distribution. 14 | # Neither the name of the MasterCard International Incorporated nor the names of its 15 | # contributors may be used to endorse or promote products derived from this software 16 | # without specific prior written permission. 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 18 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 20 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 22 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 23 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 24 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 25 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | # SUCH DAMAGE. 27 | # 28 | """ 29 | Utility file having common functions 30 | """ 31 | import base64 32 | import hashlib 33 | import time 34 | import urllib 35 | from random import SystemRandom 36 | from urllib.parse import urlparse, parse_qsl 37 | 38 | 39 | def normalize_params(url, params): 40 | """ 41 | Combines the query parameters of url and extra params into a single queryString. 42 | All the query string parameters are lexicographically sorted 43 | """ 44 | # parse the url 45 | parse = urlparse(url) 46 | 47 | # Get the query list 48 | qs_list = parse_qsl(parse.query, keep_blank_values=True) 49 | must_encode = False if parse.query == urllib.parse.unquote(parse.query) else True 50 | if params is None: 51 | combined_list = qs_list 52 | else: 53 | # Needs to be encoded before sorting 54 | combined_list = [encode_pair(must_encode, key, value) for (key, value) in list(qs_list)] 55 | combined_list += params.items() 56 | 57 | encoded_list = ["%s=%s" % (key, value) for (key, value) in combined_list] 58 | sorted_list = sorted(encoded_list, key=lambda x: x) 59 | 60 | return "&".join(sorted_list) 61 | 62 | 63 | def encode_pair(must_encode, key, value): 64 | encoded_key = percent_encode(key) if must_encode else key.replace(' ', '+') 65 | value = value if isinstance(value, bytes) else str(value) 66 | encoded_value = percent_encode(value) if must_encode else value.replace(' ', '+') 67 | return encoded_key, encoded_value 68 | 69 | 70 | def normalize_url(url): 71 | """ 72 | Removes the query parameters from the URL 73 | """ 74 | parse = urlparse(url) 75 | 76 | # netloc should be lowercase 77 | netloc = parse.netloc.lower() 78 | if parse.scheme == "http": 79 | if netloc.endswith(":80"): 80 | netloc = netloc[:-3] 81 | 82 | elif parse.scheme == "https" and netloc.endswith(":443"): 83 | netloc = netloc[:-4] 84 | 85 | # add a '/' at the end of the netloc if there in no path 86 | if not parse.path: 87 | netloc = netloc + "/" 88 | 89 | return "{}://{}{}".format(parse.scheme, netloc, parse.path) 90 | 91 | 92 | def sha256_encode(text): 93 | """ 94 | Returns the digest of SHA-256 of the text 95 | """ 96 | _hash = hashlib.sha256 97 | if type(text) is str: 98 | return _hash(text.encode('utf8')).digest() 99 | elif type(text) is bytes: 100 | return _hash(text).digest() 101 | elif not text: 102 | # Generally for calls where the payload is empty. Eg: get calls 103 | # Fix for AttributeError: 'NoneType' object has no attribute 'encode' 104 | return _hash("".encode('utf8')).digest() 105 | else: 106 | return _hash(str(text).encode('utf-8')).digest() 107 | 108 | 109 | def base64_encode(text): 110 | """ 111 | Base64 encodes the given input 112 | """ 113 | if not isinstance(text, (bytes, bytearray)): 114 | text = bytes(text.encode()) 115 | encode = base64.b64encode(text) 116 | return encode.decode('ascii') 117 | 118 | 119 | def percent_encode(text): 120 | """ 121 | Percent encodes a string as per https://tools.ietf.org/html/rfc3986 122 | """ 123 | if text is None: 124 | return '' 125 | text = text.encode('utf-8') if isinstance(text, str) else text 126 | text = urllib.parse.quote(text, safe=b'~') 127 | return text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~') 128 | 129 | 130 | def get_nonce(length=16): 131 | """ 132 | Returns a random string of length=@length 133 | """ 134 | characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' 135 | charlen = len(characters) 136 | return "".join([characters[SystemRandom().randint(0, charlen - 1)] for _ in range(0, length)]) 137 | 138 | 139 | def get_timestamp(): 140 | """ 141 | Returns the UTC timestamp (seconds passed since epoch) 142 | """ 143 | return int(time.time()) 144 | -------------------------------------------------------------------------------- /oauth1/oauth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2019-2021 Mastercard 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are 7 | # permitted provided that the following conditions are met: 8 | # 9 | # Redistributions of source code must retain the above copyright notice, this list of 10 | # conditions and the following disclaimer. 11 | # Redistributions in binary form must reproduce the above copyright notice, this list of 12 | # conditions and the following disclaimer in the documentation and/or other materials 13 | # provided with the distribution. 14 | # Neither the name of the MasterCard International Incorporated nor the names of its 15 | # contributors may be used to endorse or promote products derived from this software 16 | # without specific prior written permission. 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 18 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 20 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 22 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 23 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 24 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 25 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | # SUCH DAMAGE. 27 | # 28 | import json 29 | 30 | from cryptography.hazmat.primitives import hashes 31 | from cryptography.hazmat.primitives.asymmetric import padding 32 | 33 | import oauth1.coreutils as util 34 | 35 | 36 | class OAuth: 37 | EMPTY_STRING = "" 38 | 39 | @staticmethod 40 | def get_authorization_header(uri, method, payload, consumer_key, signing_key): 41 | oauth_parameters = OAuth.get_oauth_parameters(uri, method, payload, consumer_key, signing_key) 42 | 43 | # Get the updated base parameters dict 44 | oauth_base_parameters_dict = oauth_parameters.get_base_parameters_dict() 45 | 46 | # Generate the header value for OAuth Header 47 | oauth_key = OAuthParameters.OAUTH_KEY + " " + ",".join( 48 | [str(key) + "=\"" + str(value) + "\"" for (key, value) in oauth_base_parameters_dict.items()]) 49 | return oauth_key 50 | 51 | @staticmethod 52 | def get_oauth_parameters(uri, method, payload, consumer_key, signing_key): 53 | # Get all the base parameters such as nonce and timestamp 54 | oauth_parameters = OAuthParameters() 55 | oauth_parameters.set_oauth_consumer_key(consumer_key) 56 | oauth_parameters.set_oauth_nonce(util.get_nonce()) 57 | oauth_parameters.set_oauth_timestamp(util.get_timestamp()) 58 | oauth_parameters.set_oauth_signature_method("RSA-SHA256") 59 | oauth_parameters.set_oauth_version("1.0") 60 | 61 | payload_str = json.dumps(payload) if type(payload) is dict or type(payload) is list else payload 62 | if not payload_str: 63 | # If the request does not have an entity body, the hash should be taken over the empty string 64 | payload_str = OAuth.EMPTY_STRING 65 | 66 | encoded_hash = util.base64_encode(util.sha256_encode(payload_str)) 67 | oauth_parameters.set_oauth_body_hash(encoded_hash) 68 | 69 | # Get the base string 70 | base_string = OAuth.get_base_string(uri, method, oauth_parameters.get_base_parameters_dict()) 71 | 72 | # Sign the base string using the private key 73 | signature = OAuth.sign_message(base_string, signing_key) 74 | 75 | # Set the signature in the Base parameters 76 | oauth_parameters.set_oauth_signature(util.percent_encode(signature)) 77 | 78 | return oauth_parameters 79 | 80 | @staticmethod 81 | def get_base_string(url, method, oauth_parameters): 82 | merge_params = oauth_parameters.copy() 83 | return "{}&{}&{}".format(method.upper(), 84 | util.percent_encode(util.normalize_url(url)), 85 | util.percent_encode(util.normalize_params(url, merge_params))) 86 | 87 | @staticmethod 88 | def sign_message(message, signing_key): 89 | # Signs the message using the private signing key 90 | signature = signing_key.sign(message.encode("utf-8"), 91 | padding.PKCS1v15(), 92 | hashes.SHA256()) 93 | 94 | return util.base64_encode(signature) 95 | 96 | 97 | class OAuthParameters(object): 98 | """ 99 | Stores the OAuth parameters required to generate the Base String and Headers constants 100 | """ 101 | 102 | OAUTH_BODY_HASH_KEY = "oauth_body_hash" 103 | OAUTH_CONSUMER_KEY = "oauth_consumer_key" 104 | OAUTH_NONCE_KEY = "oauth_nonce" 105 | OAUTH_KEY = "OAuth" 106 | AUTHORIZATION = "Authorization" 107 | OAUTH_SIGNATURE_KEY = "oauth_signature" 108 | OAUTH_SIGNATURE_METHOD_KEY = "oauth_signature_method" 109 | OAUTH_TIMESTAMP_KEY = "oauth_timestamp" 110 | OAUTH_VERSION = "oauth_version" 111 | 112 | def __init__(self): 113 | self.base_parameters = {} 114 | 115 | def put(self, key, value): 116 | self.base_parameters[key] = value 117 | 118 | def get(self, key): 119 | return self.base_parameters[key] 120 | 121 | def set_oauth_consumer_key(self, consumer_key): 122 | self.put(OAuthParameters.OAUTH_CONSUMER_KEY, consumer_key) 123 | 124 | def get_oauth_consumer_key(self): 125 | return self.get(OAuthParameters.OAUTH_CONSUMER_KEY) 126 | 127 | def set_oauth_nonce(self, oauth_nonce): 128 | self.put(OAuthParameters.OAUTH_NONCE_KEY, oauth_nonce) 129 | 130 | def get_oauth_nonce(self): 131 | return self.get(OAuthParameters.OAUTH_NONCE_KEY) 132 | 133 | def set_oauth_timestamp(self, timestamp): 134 | self.put(OAuthParameters.OAUTH_TIMESTAMP_KEY, timestamp) 135 | 136 | def get_oauth_timestamp(self): 137 | return self.get(OAuthParameters.OAUTH_TIMESTAMP_KEY) 138 | 139 | def set_oauth_signature_method(self, signature_method): 140 | self.put(OAuthParameters.OAUTH_SIGNATURE_METHOD_KEY, signature_method) 141 | 142 | def get_oauth_signature_method(self): 143 | return self.get(OAuthParameters.OAUTH_SIGNATURE_METHOD_KEY) 144 | 145 | def set_oauth_signature(self, signature): 146 | self.put(OAuthParameters.OAUTH_SIGNATURE_KEY, signature) 147 | 148 | def get_oauth_signature(self): 149 | return self.get(OAuthParameters.OAUTH_SIGNATURE_KEY) 150 | 151 | def set_oauth_body_hash(self, body_hash): 152 | self.put(OAuthParameters.OAUTH_BODY_HASH_KEY, body_hash) 153 | 154 | def get_oauth_body_hash(self): 155 | return self.get(OAuthParameters.OAUTH_BODY_HASH_KEY) 156 | 157 | def set_oauth_version(self, version): 158 | self.put(OAuthParameters.OAUTH_VERSION, version) 159 | 160 | def get_oauth_version(self): 161 | return self.get(OAuthParameters.OAUTH_VERSION) 162 | 163 | def get_base_parameters_dict(self): 164 | return self.base_parameters 165 | -------------------------------------------------------------------------------- /oauth1/oauth_ext.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2020-2021 Mastercard 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are 7 | # permitted provided that the following conditions are met: 8 | # 9 | # Redistributions of source code must retain the above copyright notice, this list of 10 | # conditions and the following disclaimer. 11 | # Redistributions in binary form must reproduce the above copyright notice, this list of 12 | # conditions and the following disclaimer in the documentation and/or other materials 13 | # provided with the distribution. 14 | # Neither the name of the MasterCard International Incorporated nor the names of its 15 | # contributors may be used to endorse or promote products derived from this software 16 | # without specific prior written permission. 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 18 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 20 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 22 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 23 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 24 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 25 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | # SUCH DAMAGE. 27 | # 28 | from requests import PreparedRequest 29 | from requests.auth import AuthBase 30 | 31 | from .oauth import OAuth 32 | 33 | 34 | class OAuth1RSA(AuthBase): 35 | """OAuth1 RSA-SHA256 requests's auth helper 36 | Usage: 37 | >>> from oauth1 import authenticationutils 38 | >>> from oauth1.oauth_ext import OAuth1RSA 39 | >>> import requests 40 | >>> CONSUMER_KEY = 'secret-consumer-key' 41 | >>> pk = authenticationutils.load_signing_key('instance/masterpass.pfx', 'a3fa02536a') 42 | >>> oauth = OAuth1RSA(CONSUMER_KEY, pk) 43 | >>> requests.post('https://endpoint.com/the/route', data={'foo': 'bar'}, auth=oauth) 44 | """ 45 | 46 | def __init__(self, consumer_key: str, signing_key: bytes): 47 | self.consumer_key = consumer_key 48 | self.signing_key = signing_key 49 | 50 | def __call__(self, r: PreparedRequest): 51 | method = r.method.upper() if r.method is not None else r.method 52 | if all(v is not None for v in [r, self.consumer_key, self.signing_key]): 53 | r.headers['Authorization'] = \ 54 | OAuth.get_authorization_header(uri=r.url, 55 | method=method, 56 | payload=r.body, 57 | consumer_key=self.consumer_key, 58 | signing_key=self.signing_key) 59 | return r 60 | -------------------------------------------------------------------------------- /oauth1/signer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2019-2021 Mastercard 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are 7 | # permitted provided that the following conditions are met: 8 | # 9 | # Redistributions of source code must retain the above copyright notice, this list of 10 | # conditions and the following disclaimer. 11 | # Redistributions in binary form must reproduce the above copyright notice, this list of 12 | # conditions and the following disclaimer in the documentation and/or other materials 13 | # provided with the distribution. 14 | # Neither the name of the MasterCard International Incorporated nor the names of its 15 | # contributors may be used to endorse or promote products derived from this software 16 | # without specific prior written permission. 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 18 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 20 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 22 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 23 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 24 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 25 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | # SUCH DAMAGE. 27 | # 28 | from requests import PreparedRequest 29 | 30 | from oauth1.oauth import OAuth 31 | 32 | 33 | class OAuthSigner: 34 | 35 | def __init__(self, consumer_key, signing_key): 36 | self.consumer_key = consumer_key 37 | self.signing_key = signing_key 38 | 39 | def sign_request(self, uri, request): 40 | body = request.body if isinstance(request, PreparedRequest) else request.data 41 | # Generates the OAuth header for the request, adds the header to the request and returns the request object 42 | oauth_key = OAuth.get_authorization_header(uri, request.method, body, self.consumer_key, 43 | self.signing_key) 44 | request.headers["Authorization"] = oauth_key 45 | return request 46 | -------------------------------------------------------------------------------- /oauth1/signer_interceptor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2019-2021 Mastercard 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, are 7 | # permitted provided that the following conditions are met: 8 | # 9 | # Redistributions of source code must retain the above copyright notice, this list of 10 | # conditions and the following disclaimer. 11 | # Redistributions in binary form must reproduce the above copyright notice, this list of 12 | # conditions and the following disclaimer in the documentation and/or other materials 13 | # provided with the distribution. 14 | # Neither the name of the MasterCard International Incorporated nor the names of its 15 | # contributors may be used to endorse or promote products derived from this software 16 | # without specific prior written permission. 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 18 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 20 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 22 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 23 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 24 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 25 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | # SUCH DAMAGE. 27 | # 28 | from functools import wraps 29 | from urllib.parse import urlencode 30 | 31 | from deprecated import deprecated 32 | 33 | from oauth1 import authenticationutils 34 | from oauth1.oauth import OAuth 35 | 36 | 37 | class SignerInterceptor(object): 38 | 39 | def __init__(self, key_file, key_password, consumer_key): 40 | """Load signing key.""" 41 | self.signing_key = authenticationutils.load_signing_key(key_file, key_password) 42 | self.consumer_key = consumer_key 43 | 44 | def oauth_signing(self, func): 45 | """Decorator for API request. func is APIClient.request""" 46 | 47 | @wraps(func) 48 | def request_function(*args, **kwargs): # pragma: no cover 49 | in_body = kwargs.get("body", None) 50 | query_params = kwargs.get("query_params", None) 51 | 52 | uri = args[1] 53 | if query_params: 54 | uri += '?' + urlencode(query_params) 55 | 56 | auth_header = OAuth.get_authorization_header(uri, args[0], in_body, self.consumer_key, self.signing_key) 57 | 58 | in_headers = kwargs.get("headers", None) 59 | if not in_headers: 60 | in_headers = dict() 61 | kwargs["headers"] = in_headers 62 | 63 | in_headers["Authorization"] = auth_header 64 | 65 | res = func(*args, **kwargs) 66 | 67 | return res 68 | 69 | request_function.__oauth__ = True 70 | return request_function 71 | 72 | 73 | @deprecated(version='1.1.3', reason="Use add_signer_layer(api_client, key_file, key_password, consumer_key) instead") 74 | def add_signing_layer(self, api_client, key_file, key_password, consumer_key): 75 | add_signer_layer(api_client, key_file, key_password, consumer_key) 76 | 77 | 78 | def add_signer_layer(api_client, key_file, key_password, consumer_key): 79 | """Create and load configuration. Decorate APIClient.request with header signing""" 80 | 81 | api_signer = SignerInterceptor(key_file, key_password, consumer_key) 82 | 83 | api_client.rest_client.request = api_signer.oauth_signing(api_client.rest_client.request) 84 | 85 | 86 | @deprecated(version='1.1.3', reason="Use get_signer_layer(api_client) instead") 87 | def get_signing_layer(self, api_client): 88 | return get_signer_layer(api_client) 89 | 90 | 91 | def get_signer_layer(api_client): 92 | return api_client.rest_client.request 93 | -------------------------------------------------------------------------------- /oauth1/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = '1.9.0' 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.0 2 | Deprecated==1.2.5 3 | cryptography>=42.0.0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019-2024 Mastercard 3 | # 4 | # Redistribution and use in source and binary forms, with or without modification, are 5 | # permitted provided that the following conditions are met: 6 | # 7 | # Redistributions of source code must retain the above copyright notice, this list of 8 | # conditions and the following disclaimer. 9 | # Redistributions in binary form must reproduce the above copyright notice, this list of 10 | # conditions and the following disclaimer in the documentation and/or other materials 11 | # provided with the distribution. 12 | # Neither the name of the MasterCard International Incorporated nor the names of its 13 | # contributors may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 18 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | # SUCH DAMAGE. 25 | # 26 | 27 | from setuptools import setup, find_packages 28 | 29 | exec(open('oauth1/version.py').read()) 30 | 31 | setup(name='mastercard-oauth1-signer', 32 | version=__version__, 33 | description='Mastercard OAuth1 Signer.', 34 | long_description='Python library for generating a Mastercard API compliant OAuth signature. ', 35 | author='Mastercard', 36 | url='https://github.com/Mastercard/oauth1-signer-python', 37 | license='MIT', 38 | packages=find_packages(), 39 | classifiers=[ 40 | 'Intended Audience :: Developers', 41 | 'Natural Language :: English', 42 | 'Operating System :: OS Independent', 43 | 'Programming Language :: Python :: 3.6', 44 | 'Programming Language :: Python :: 3.7', 45 | 'Programming Language :: Python :: 3.8', 46 | 'Topic :: Software Development :: Libraries :: Python Modules' 47 | ], 48 | tests_require=['coverage'], 49 | install_requires=['requests','cryptography>=42.0.0','urllib3', 'Deprecated'] 50 | ) 51 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Mastercard_oauth1-signer-python 2 | sonar.organization=mastercard 3 | sonar.projectName=oauth1-signer-python 4 | sonar.sources=./oauth1 5 | sonar.tests=./tests 6 | sonar.python.coverage.reportPaths=coverage.xml 7 | sonar.host.url=https://sonarcloud.io -------------------------------------------------------------------------------- /test_key_container.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/oauth1-signer-python/c084f2e42fdf0aa648321bf137dc26de441f21f4/test_key_container.p12 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard/oauth1-signer-python/c084f2e42fdf0aa648321bf137dc26de441f21f4/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_interceptor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*-# 2 | # 3 | # 4 | # Copyright 2019-2021 Mastercard 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without modification, are 8 | # permitted provided that the following conditions are met: 9 | # 10 | # Redistributions of source code must retain the above copyright notice, this list of 11 | # conditions and the following disclaimer. 12 | # Redistributions in binary form must reproduce the above copyright notice, this list of 13 | # conditions and the following disclaimer in the documentation and/or other materials 14 | # provided with the distribution. 15 | # Neither the name of the MasterCard International Incorporated nor the names of its 16 | # contributors may be used to endorse or promote products derived from this software 17 | # without specific prior written permission. 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 20 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 21 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 22 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 23 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 24 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 25 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 26 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 | # SUCH DAMAGE. 28 | # 29 | import unittest 30 | import requests 31 | from oauth1.signer_interceptor import add_signer_layer 32 | from oauth1.signer_interceptor import get_signer_layer 33 | 34 | 35 | class OAuthInterceptorTest(unittest.TestCase): 36 | """ add an interceptor, check api client has changed """ 37 | 38 | def test_add_interceptor(self): 39 | key_file = './test_key_container.p12' 40 | key_password = "Password1" 41 | consumer_key = 'dummy' 42 | 43 | signer_request = MockApiRestClient(requests) 44 | 45 | signing_layer1 = get_signer_layer(signer_request) 46 | add_signer_layer(signer_request, key_file, key_password, consumer_key) 47 | signing_layer2 = get_signer_layer(signer_request) 48 | self.assertNotEqual(signing_layer1, signing_layer2) 49 | 50 | 51 | class MockApiRestClient(object): 52 | def __init__(self, request): 53 | self.request = request 54 | self.rest_client = request 55 | 56 | 57 | if __name__ == '__main__': 58 | unittest.main() 59 | 60 | def __del__(self): 61 | self.child.terminate() 62 | self.child.communicate() 63 | -------------------------------------------------------------------------------- /tests/test_oauth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*-# 2 | # 3 | # 4 | # Copyright 2019-2021 Mastercard 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without modification, are 8 | # permitted provided that the following conditions are met: 9 | # 10 | # Redistributions of source code must retain the above copyright notice, this list of 11 | # conditions and the following disclaimer. 12 | # Redistributions in binary form must reproduce the above copyright notice, this list of 13 | # conditions and the following disclaimer in the documentation and/or other materials 14 | # provided with the distribution. 15 | # Neither the name of the MasterCard International Incorporated nor the names of its 16 | # contributors may be used to endorse or promote products derived from this software 17 | # without specific prior written permission. 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 20 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 21 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 22 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 23 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 24 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 25 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 26 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 | # SUCH DAMAGE. 28 | # 29 | import unittest 30 | from collections import Counter 31 | from unittest.mock import MagicMock 32 | 33 | import oauth1.authenticationutils as authenticationutils 34 | import oauth1.coreutils as util 35 | from importlib import reload 36 | from oauth1.oauth import OAuth 37 | from oauth1.oauth import OAuthParameters 38 | 39 | 40 | class OAuthTest(unittest.TestCase): 41 | signing_key = authenticationutils.load_signing_key('./test_key_container.p12', "Password1") 42 | uri = 'https://www.example.com' 43 | 44 | def test_get_authorization_header_nominal(self): 45 | header = OAuth.get_authorization_header(OAuthTest.uri, 'POST', 'payload', 'dummy', OAuthTest.signing_key) 46 | self.assertTrue("OAuth" in header) 47 | self.assertTrue("dummy" in header) 48 | 49 | def test_get_authorization_header_should_compute_body_hash(self): 50 | header = OAuth.get_authorization_header(OAuthTest.uri, 'POST', '{}', 'dummy', OAuthTest.signing_key) 51 | self.assertTrue('RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' in header) 52 | 53 | def test_get_authorization_header_should_return_empty_string_body_hash(self): 54 | header = OAuth.get_authorization_header(OAuthTest.uri, 'GET', None, 'dummy', OAuthTest.signing_key) 55 | self.assertTrue('47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' in header) 56 | 57 | def test_get_nonce(self): 58 | nonce = util.get_nonce() 59 | self.assertEqual(len(nonce), 16) 60 | 61 | def test_get_timestamp(self): 62 | timestamp = util.get_timestamp() 63 | self.assertEqual(len(str(timestamp)), 10) 64 | 65 | def test_sign_message(self): 66 | base_string = 'POST&https%3A%2F%2Fsandbox.api.mastercard.com%2Ffraud%2Fmerchant%2Fv1%2Ftermination-inquiry' \ 67 | '&Format%3DXML%26PageLength%3D10%26PageOffset%3D0%26oauth_body_hash%3DWhqqH' \ 68 | '%252BTU95VgZMItpdq78BWb4cE%253D%26oauth_consumer_key' \ 69 | '%3Dxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx%26oauth_nonce%3D1111111111111111111' \ 70 | '%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1111111111%26oauth_version%3D1.0' 71 | signature = OAuth.sign_message(base_string, OAuthTest.signing_key) 72 | signature = util.percent_encode(signature) 73 | self.assertEqual(signature, 74 | "DvyS3R795sUb%2FcvBfiFYZzPDU%2BRVefW6X%2BAfyu%2B9fxjudQft" 75 | "%2BShXhpounzJxYCwOkkjZWXOR0ICTMn6MOuG04TTtmPMrOxj5feGwD3leMBsi" 76 | "%2B3XxcFLPi8BhZKqgapcAqlGfjEhq0COZ%2FF9aYDcjswLu0zgrTMSTp4cqXYMr9mbQVB4HL" 77 | "%2FjiHni5ejQu9f6JB9wWW%2BLXYhe8F6b4niETtzIe5o77%2B" 78 | "%2BkKK67v9wFIZ9pgREz7ug8K5DlxX0DuwdUKFhsenA5z%2FNNCZrJE" 79 | "%2BtLu0tSjuF5Gsjw5GRrvW33MSoZ0AYfeleh5V3nLGgHrhVjl5%2BiS40pnG2po%2F5hIAUT5ag%3D%3D") 80 | 81 | def test_oauth_parameters(self): 82 | uri = "https://sandbox.api.mastercard.com/fraud/merchant/v1/termination-inquiry?Format=XML&PageOffset=0" 83 | method = "POST" 84 | parameters = OAuth.get_oauth_parameters(uri, method, 'payload', 'dummy', OAuthTest.signing_key) 85 | consumer_key = parameters.get_oauth_consumer_key() 86 | self.assertEqual("dummy", consumer_key) 87 | 88 | def test_query_parser(self): 89 | uri = "https://sandbox.api.mastercard.com/audiences/v1/getcountries?offset=0&offset=1&length=10&empty&odd=" 90 | oauth_parameters = OAuthParameters() 91 | oauth_parameters_base = oauth_parameters.get_base_parameters_dict() 92 | merge_parameters = oauth_parameters_base.copy() 93 | query_params = util.normalize_params(uri, merge_parameters) 94 | self.assertEqual(query_params, "empty=&length=10&odd=&offset=0&offset=1") 95 | 96 | def test_query_parser_when_params_is_None(self): 97 | uri = "https://sandbox.api.mastercard.com/audiences/v1/getcountries" 98 | query_params = util.normalize_params(uri, None) 99 | self.assertEqual(query_params, '') 100 | 101 | def test_query_parser_encoding(self): 102 | uri = "https://sandbox.api.mastercard.com?param1=plus+value¶m2=colon:value" 103 | oauth_parameters = OAuthParameters() 104 | oauth_parameters_base = oauth_parameters.get_base_parameters_dict() 105 | merge_parameters = oauth_parameters_base.copy() 106 | query_params = util.normalize_params(uri, merge_parameters) 107 | self.assertEqual(query_params, "param1=plus+value¶m2=colon:value") 108 | 109 | def test_nonce_length(self): 110 | nonce = util.get_nonce() 111 | self.assertEqual(16, len(nonce)) 112 | 113 | def test_nonce_uniqueness(self): 114 | list_of_nonce = [] 115 | 116 | for _ in range(0, 100000): 117 | list_of_nonce.append(util.get_nonce()) 118 | 119 | counter = Counter(list_of_nonce) 120 | res = [k for k, v in counter.items() if v > 1] 121 | 122 | self.assertEqual(len(res), 0) 123 | 124 | def test_params_string_rfc_example_1(self): 125 | uri = "https://sandbox.api.mastercard.com" 126 | 127 | oauth_parameters1 = OAuthParameters() 128 | oauth_parameters1.set_oauth_consumer_key("9djdj82h48djs9d2") 129 | oauth_parameters1.set_oauth_signature_method("HMAC-SHA1") 130 | oauth_parameters1.set_oauth_timestamp("137131201") 131 | oauth_parameters1.set_oauth_nonce("7d8f3e4a") 132 | 133 | oauth_parameters_base1 = oauth_parameters1.get_base_parameters_dict() 134 | merge_parameters1 = oauth_parameters_base1.copy() 135 | query_params1 = util.normalize_params(uri, merge_parameters1) 136 | 137 | self.assertEqual( 138 | "oauth_consumer_key=9djdj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1" 139 | "&oauth_timestamp=137131201", 140 | query_params1) 141 | 142 | def test_params_string_rfc_example_2(self): 143 | uri = "https://sandbox.api.mastercard.com?b5=%3D%253D&a3=a&a3=2%20q&c%40=&a2=r%20b&c2=" 144 | 145 | oauth_parameters2 = OAuthParameters() 146 | oauth_parameters_base2 = oauth_parameters2.get_base_parameters_dict() 147 | merge_parameters2 = oauth_parameters_base2.copy() 148 | query_params2 = util.normalize_params(uri, merge_parameters2) 149 | 150 | self.assertEqual("a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=", query_params2) 151 | 152 | def test_params_string_ascending_byte_value_ordering(self): 153 | url = "https://localhost?b=b&A=a&A=A&B=B&a=A&a=a&0=0" 154 | 155 | oauth_parameters = OAuthParameters() 156 | oauth_parameters_base = oauth_parameters.get_base_parameters_dict() 157 | merge_parameters = oauth_parameters_base.copy() 158 | norm_params = util.normalize_params(url, merge_parameters) 159 | 160 | self.assertEqual("0=0&A=A&A=a&B=B&a=A&a=a&b=b", norm_params) 161 | 162 | def test_signature_base_string(self): 163 | uri = "https://api.mastercard.com" 164 | base_uri = util.normalize_url(uri) 165 | 166 | oauth_parameters = OAuthParameters() 167 | oauth_parameters.set_oauth_body_hash("body/hash") 168 | oauth_parameters.set_oauth_nonce("randomnonce") 169 | 170 | base_string = OAuth.get_base_string(base_uri, "POST", oauth_parameters.get_base_parameters_dict()) 171 | self.assertEqual( 172 | "POST&https%3A%2F%2Fapi.mastercard.com%2F&oauth_body_hash%3Dbody%2Fhash%26oauth_nonce%3Drandomnonce", 173 | base_string) 174 | 175 | def test_signature_base_string2(self): 176 | body = "19961TESTTEST55555555551234567890
5555 Test " \ 181 | "LaneTESTXX12345USA
JohnSmith12345678905555555555
5555 Test " \ 184 | "LaneTESTXX12345USA
1234567890XX" \ 186 | "
" 187 | url = "https://sandbox.api.mastercard.com/fraud/merchant/v1/termination-inquiry?Format=XML&PageOffset=0" \ 188 | "&PageLength=10" 189 | method = "POST" 190 | 191 | oauth_parameters = OAuthParameters() 192 | oauth_parameters.set_oauth_consumer_key("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") 193 | oauth_parameters.set_oauth_nonce("1111111111111111111") 194 | oauth_parameters.set_oauth_timestamp("1111111111") 195 | oauth_parameters.set_oauth_version("1.0") 196 | oauth_parameters.set_oauth_body_hash("body/hash") 197 | encoded_hash = util.base64_encode(util.sha256_encode(body)) 198 | oauth_parameters.set_oauth_body_hash(encoded_hash) 199 | 200 | base_string = OAuth.get_base_string(url, method, oauth_parameters.get_base_parameters_dict()) 201 | expected = "POST&https%3A%2F%2Fsandbox.api.mastercard.com%2Ffraud%2Fmerchant%2Fv1%2Ftermination-inquiry" \ 202 | "&Format%3DXML%26PageLength%3D10%26PageOffset%3D0%26oauth_body_hash%3Dh2Pd7zlzEZjZVIKB4j94UZn" \ 203 | "%2FxxoR3RoCjYQ9%2FJdadGQ%3D%26oauth_consumer_key" \ 204 | "%3Dxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx%26oauth_nonce%3D1111111111111111111" \ 205 | "%26oauth_timestamp%3D1111111111%26oauth_version%3D1.0" 206 | 207 | self.assertEqual(expected, base_string) 208 | 209 | def test_sign_signature_base_string_invalid_key(self): 210 | self.assertRaises(AttributeError, OAuth.sign_message, "some string", None) 211 | 212 | def test_sign_signature_base_string(self): 213 | expected_signature_string = "IJeNKYGfUhFtj5OAPRI92uwfjJJLCej3RCMLbp7R6OIYJhtwxnTkloHQ2bgV7fks4GT" \ 214 | "/A7rkqrgUGk0ewbwIC6nS3piJHyKVc7rvQXZuCQeeeQpFzLRiH3rsb+ZS+AULK+jzDje4Fb" \ 215 | "+BQR6XmxuuJmY6YrAKkj13Ln4K6bZJlSxOizbNvt+Htnx" \ 216 | "+hNd4VgaVBeJKcLhHfZbWQxK76nMnjY7nDcM/2R6LUIR2oLG1L9m55WP3bakAvmOr392ulv1" \ 217 | "+mWCwDAZZzQ4lakDD2BTu0ZaVsvBW+mcKFxYeTq7SyTQMM4lEwFPJ6RLc8jJJ" \ 218 | "+veJXHekLVzWg4qHRtzNBLz1mA==" 219 | signing_string = OAuth.sign_message("baseString", OAuthTest.signing_key) 220 | self.assertEqual(expected_signature_string, signing_string) 221 | 222 | def test_url_normalization_rfc_examples1(self): 223 | uri = "https://www.example.net:8080" 224 | base_uri = util.normalize_url(uri) 225 | self.assertEqual("https://www.example.net:8080/", base_uri) 226 | 227 | def test_url_normalization_rfc_examples2(self): 228 | uri = "http://EXAMPLE.COM:80/r%20v/X?id=123" 229 | base_uri = util.normalize_url(uri) 230 | self.assertEqual("http://example.com/r%20v/X", base_uri) 231 | 232 | def test_url_normalization_redundant_ports1(self): 233 | uri = "https://api.mastercard.com:443/test?query=param" 234 | base_uri = util.normalize_url(uri) 235 | self.assertEqual("https://api.mastercard.com/test", base_uri) 236 | 237 | def test_url_normalization_redundant_ports2(self): 238 | uri = "http://api.mastercard.com:80/test" 239 | base_uri = util.normalize_url(uri) 240 | self.assertEqual("http://api.mastercard.com/test", base_uri) 241 | 242 | def test_url_normalization_redundant_ports3(self): 243 | uri = "https://api.mastercard.com:17443/test?query=param" 244 | base_uri = util.normalize_url(uri) 245 | self.assertEqual("https://api.mastercard.com:17443/test", base_uri) 246 | 247 | def test_url_normalization_remove_fragment(self): 248 | uri = "https://api.mastercard.com/test?query=param#fragment" 249 | base_uri = util.normalize_url(uri) 250 | self.assertEqual("https://api.mastercard.com/test", base_uri) 251 | 252 | def test_url_normalization_add_trailing_slash(self): 253 | uri = "https://api.mastercard.com" 254 | base_uri = util.normalize_url(uri) 255 | self.assertEqual("https://api.mastercard.com/", base_uri) 256 | 257 | def test_url_normalization_lowercase_scheme_and_host(self): 258 | uri = "HTTPS://API.MASTERCARD.COM/TEST" 259 | base_uri = util.normalize_url(uri) 260 | self.assertEqual("https://api.mastercard.com/TEST", base_uri) 261 | 262 | def test_body_hash1(self): 263 | oauth_parameters = OAuthParameters() 264 | encoded_hash = util.base64_encode(util.sha256_encode(OAuth.EMPTY_STRING)) 265 | oauth_parameters.set_oauth_body_hash(encoded_hash) 266 | self.assertEqual("47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", encoded_hash) 267 | 268 | def test_body_hash2(self): 269 | oauth_parameters = OAuthParameters() 270 | encoded_hash = util.base64_encode(util.sha256_encode(None)) 271 | oauth_parameters.set_oauth_body_hash(encoded_hash) 272 | self.assertEqual("47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", encoded_hash) 273 | 274 | def test_body_hash3(self): 275 | oauth_parameters = OAuthParameters() 276 | encoded_hash = util.base64_encode(util.sha256_encode("{\"foõ\":\"bar\"}")) 277 | oauth_parameters.set_oauth_body_hash(encoded_hash) 278 | self.assertEqual("+Z+PWW2TJDnPvRcTgol+nKO3LT7xm8smnsg+//XMIyI=", encoded_hash) 279 | 280 | def test_url_encode1(self): 281 | self.assertEqual("Format%3DXML", util.percent_encode("Format=XML")) 282 | 283 | def test_url_encode2(self): 284 | self.assertEqual("WhqqH%2BTU95VgZMItpdq78BWb4cE%3D", util.percent_encode("WhqqH+TU95VgZMItpdq78BWb4cE=")) 285 | 286 | def test_url_encode3(self): 287 | self.assertEqual("WhqqH%2BTU95VgZMItpdq78BWb4cE%3D%26o", 288 | util.percent_encode("WhqqH+TU95VgZMItpdq78BWb4cE=&o")) 289 | 290 | def test_get_oauth_nonce_param(self): 291 | oauth_parameters = OAuthParameters() 292 | oauth_parameters.put(OAuthParameters.OAUTH_NONCE_KEY, "abcde") 293 | val = oauth_parameters.get_oauth_nonce() 294 | self.assertEqual("abcde", val) 295 | 296 | def test_get_oauth_nonce_timestamp_param(self): 297 | oauth_parameters = OAuthParameters() 298 | oauth_parameters.put(OAuthParameters.OAUTH_TIMESTAMP_KEY, "abcde") 299 | val = oauth_parameters.get_oauth_timestamp() 300 | self.assertEqual("abcde", val) 301 | 302 | def test_get_oauth_signature_method_param(self): 303 | oauth_parameters = OAuthParameters() 304 | oauth_parameters.put(OAuthParameters.OAUTH_SIGNATURE_METHOD_KEY, "abcde") 305 | val = oauth_parameters.get_oauth_signature_method() 306 | self.assertEqual("abcde", val) 307 | 308 | def test_get_oauth_signature_param(self): 309 | oauth_parameters = OAuthParameters() 310 | oauth_parameters.put(OAuthParameters.OAUTH_SIGNATURE_KEY, "abcde") 311 | val = oauth_parameters.get_oauth_signature() 312 | self.assertEqual("abcde", val) 313 | 314 | def test_get_oauth_body_hash_param(self): 315 | oauth_parameters = OAuthParameters() 316 | oauth_parameters.put(OAuthParameters.OAUTH_BODY_HASH_KEY, "abcde") 317 | val = oauth_parameters.get_oauth_body_hash() 318 | self.assertEqual("abcde", val) 319 | 320 | def test_get_oauth_version_param(self): 321 | oauth_parameters = OAuthParameters() 322 | oauth_parameters.put(OAuthParameters.OAUTH_VERSION, "abcde") 323 | val = oauth_parameters.get_oauth_version() 324 | self.assertEqual("abcde", val) 325 | 326 | def test_backward_compatibility_with_static_method(self): 327 | header = OAuth().get_authorization_header(OAuthTest.uri, 'POST', 'payload', 'dummy', OAuthTest.signing_key) 328 | self.assertTrue("OAuth" in header) 329 | self.assertTrue("dummy" in header) 330 | 331 | header = OAuth.get_authorization_header(OAuthTest.uri, 'POST', 'payload', 'dummy', OAuthTest.signing_key) 332 | self.assertTrue("OAuth" in header) 333 | self.assertTrue("dummy" in header) 334 | 335 | def test_percent_encoding(self): 336 | self.assertEqual("Format%3DXML", util.percent_encode("Format=XML")) 337 | self.assertEqual("WhqqH%2BTU95VgZMItpdq78BWb4cE%3D", util.percent_encode("WhqqH+TU95VgZMItpdq78BWb4cE=")) 338 | self.assertEqual("WhqqH%2BTU95VgZMItpdq78BWb4cE%3D%26o", util.percent_encode("WhqqH+TU95VgZMItpdq78BWb4cE=&o")) 339 | self.assertEqual("WhqqH%2BTU95VgZ~Itpdq78BWb4cE%3D%26o", util.percent_encode("WhqqH+TU95VgZ~Itpdq78BWb4cE=&o")) 340 | self.assertEqual("%2525%C2%A3%C2%A5a%2FEl%20Ni%C3%B1o%2F%25", util.percent_encode("%25£¥a/El Niño/%")) 341 | 342 | def test_valid_oauth_signature_with_percent(self): 343 | util.get_nonce = MagicMock(return_value='Wpe3LF09z1e3xQRI') 344 | util.get_timestamp = MagicMock(return_value=1626728330) 345 | auth_header = OAuth.get_authorization_header('https://api.mastercard.com/abc/%123/service?a=123&b=%2a2b3', 346 | 'GET', None, 347 | 'abc-abc-abc!123', OAuthTest.signing_key) 348 | reload(util) 349 | 350 | self.assertEqual('OAuth oauth_consumer_key="abc-abc-abc!123",oauth_nonce="Wpe3LF09z1e3xQRI",' 351 | 'oauth_timestamp="1626728330",oauth_signature_method="RSA-SHA256",oauth_version="1.0",' 352 | 'oauth_body_hash="47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",' 353 | 'oauth_signature="GFdvCNe14%2FQdPi6KLgdFtYnqz9QVsqlzRwT1P5wvZCgyBzfTol69SRz4cIkeDx' 354 | '%2BIEeUfkbrPdVA9JPy0S9lgpMzs0KfIAX064Bz5mBbTni8NWD74ulN5eEDQRWB47BqEsvNPSJlJLGapVe' 355 | 'YFyRlcIU7xMU1e1lA%2FtPTTHDSmIBfq4CtpCPvYMcd7ywoiHsi4hfI0d%2BTGS9pe0ez00mkne8C3%2FAHt' 356 | 'uRIp564D02Hhl6s%2BTUGdUvlXTaFaIH9GVdZ15n%2FUcTCqSKFjorwA9guiJQlFpZtQy04BBD19VbN6%2F%2BS' 357 | 'JvMAnVFQM5FJhgZ%2F5T9OP9%2BmjXz47EhG9MAx3raBjIw%3D%3D"', auth_header) 358 | 359 | def test_auth_header_when_uri_created_with_encoded_params(self): 360 | url = 'https://example.com/request?colon=%3A&plus=%2B&comma=%2C' 361 | util.get_nonce = MagicMock(return_value='Wpe3LF09z1e3xQRI') 362 | util.get_timestamp = MagicMock(return_value=1626728330) 363 | auth_header = OAuth.get_authorization_header(url, 364 | 'GET', None, 365 | 'abc-abc-abc!123', OAuthTest.signing_key) 366 | reload(util) 367 | self.assertEqual('OAuth oauth_consumer_key="abc-abc-abc!123",oauth_nonce="Wpe3LF09z1e3xQRI",' 368 | 'oauth_timestamp="1626728330",oauth_signature_method="RSA-SHA256",oauth_version="1.0",' 369 | 'oauth_body_hash="47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",' 370 | 'oauth_signature="BJGTHj7bxDWKRpES4KyLxrg0jTgk11b8RCdQzOMbY%2BQoltCaocwk3' 371 | '%2BI1MlYyX5oT8xMCKcjvH6EhF2J%2BMojheBdomDuNqVlr7NvS0uRjbD1Iem%2Bo0RXMU' 372 | '%2Bag62XBMYnGdzhk3Nr1Ifwsb1seTND%2B%2Bf%2BDFjAWoD7UoY' 373 | '%2Fo2aWg1xbkXgKtykV1QIfKRsZfyJJUARUB6yhnMegryPURrGI8yAwoxGI37o0RsbCQ' 374 | 'drnzIdpQYm6C5a9FPhPTqREAXEMpBVvY2e3Fk922IXiAd6Ph%2FLdIAOnIE8RRXmc5gYmzf' 375 | 'tl8jcztjG4EJNhD2jk6YVG5tt9yq%2FrcvbokgDnZ%2F7qPeg%3D%3D"', auth_header) 376 | 377 | url = 'https://example.com/?param=token1%3Atoken2' 378 | util.get_nonce = MagicMock(return_value='Wpe3LF09z1e3xQRI') 379 | util.get_timestamp = MagicMock(return_value=1626728330) 380 | auth_header = OAuth.get_authorization_header(url, 381 | 'GET', None, 382 | 'abc-abc-abc!123', OAuthTest.signing_key) 383 | reload(util) 384 | self.assertEqual('OAuth oauth_consumer_key="abc-abc-abc!123",oauth_nonce="Wpe3LF09z1e3xQRI",' 385 | 'oauth_timestamp="1626728330",oauth_signature_method="RSA-SHA256",oauth_version="1.0",' 386 | 'oauth_body_hash="47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",' 387 | 'oauth_signature="B473PPrHLU4KIJ5763HLGk6OR2Lo5FPXTXN72K5ihc4cZMYkfAUxAfANJU' 388 | 'oKx5fHrcNPkGWeLkLdpqRLXfMNi39tB3WNOKZqYd0AgeAH9OkhFnJ0J%2FJ8oiXsaTcWK1tBm%2B' 389 | 'PMtIFaAzA1MhILuns8p1GVPBOCK4ZAfdHMOf19TVV7uCO%2BwaeQgEmzNsGt6L6%2FgIRwpFnTwr9i' 390 | 'EQCWju9LCxHpRDJIzA%2Fx4JT%2BRn5fOa3KyjPJkY70EPWmvMhdciBVYNpv%2BjEjPmrQTNN0RZDY' 391 | 'RPX%2Buj6ZRspAo%2BwHQDqAU3Fd1%2BD4lBEjY9fmK%2B3tz%2B9Ckhk%2FOfDvyIhSY4BtvsNoag' 392 | '%3D%3D"', auth_header) 393 | 394 | def test_auth_header_when_uri_created_with_non_encoded_params(self): 395 | url = 'https://example.com/?param=token1:token2' 396 | util.get_nonce = MagicMock(return_value='Wpe3LF09z1e3xQRI') 397 | util.get_timestamp = MagicMock(return_value=1626728330) 398 | auth_header = OAuth.get_authorization_header(url, 399 | 'GET', None, 400 | 'abc-abc-abc!123', OAuthTest.signing_key) 401 | reload(util) 402 | self.assertEqual('OAuth oauth_consumer_key="abc-abc-abc!123",oauth_nonce="Wpe3LF09z1e3xQRI",' 403 | 'oauth_timestamp="1626728330",oauth_signature_method="RSA-SHA256",oauth_version="1.0",' 404 | 'oauth_body_hash="47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",' 405 | 'oauth_signature="iD%2FTWBltcpIpyWJY7vDLaB2fjjEKVuBvhQje5OTOX0Cx6q%2BJnIEeRjkWx' 406 | 'cmy4UclR2hn3zugQv9IIzPuQzOMnHZA%2FyS%2BZQRtY1pR2DgWSifTr0mkiIuVNB5zNcc1ZvFbjqY' 407 | '5I7u5%2Bd1tseEWchRLUbuUZDiQP7XVdfsjJpfynDi3qbU67naKXf7HWhR%2Fbg4AglVH8xxn0hUqy' 408 | 'Ms5uHk8pmx4%2BUGzgtzDT3vs5%2FZqUALZiElm9oq0DvWvY5cgRVm%2FyCPvPBIz%2BD3e8RSKtbi' 409 | 'ZXCqzJF6zddvyUOOmp0nrso065LsvG6PLR2DYjE62XIFXy1urqiMUoHu2f52YpEzCGg%3D%3D"', 410 | auth_header) 411 | 412 | def test_auth_header_when_uri_created_with_non_encoded_params_2(self): 413 | url = "https://sandbox.api.mastercard.com?param1=plus+value¶m2=colon:value" 414 | util.get_nonce = MagicMock(return_value='Wpe3LF09z1e3xQRI') 415 | util.get_timestamp = MagicMock(return_value=1626728330) 416 | auth_header = OAuth.get_authorization_header(url, 417 | 'GET', None, 418 | 'abc-abc-abc!123', OAuthTest.signing_key) 419 | reload(util) 420 | self.assertEqual('OAuth oauth_consumer_key="abc-abc-abc!123",oauth_nonce="Wpe3LF09z1e3xQRI",' 421 | 'oauth_timestamp="1626728330",oauth_signature_method="RSA-SHA256",oauth_version="1.0",' 422 | 'oauth_body_hash="47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",' 423 | 'oauth_signature="BSbz5UAdn1iRFpFCs0y6U4HhBCm4gR56690cYyGVvkYoqcYB4PsMrurMu8aojktsKmgz0o' 424 | 'YM77YylUJMVlbWclwW2I1hexLfErvGWA91AsJT557g6kbV9ON8daDy1u33LezMjTrrmErSb%2BMtgLQ5NE8pAwo4' 425 | 'tPDBx33rjckZ7SPewrZS63EQAkc6wjt%2BnWhzkRU8%2Fuze0cLUemaVExHSwUULV38OXxOxOa3VBrBi2p%2FyEF' 426 | 'qKgTWXJmNlZ2nzHsZVcwE2TNJdZjLP0bHn2tg3MRi112u51Tag5bT4RrkwkCg6gcGc9Pn6gxIgH%2FFWBCbjgdBnR' 427 | '0plo3Z9SX3uQcDrvw%3D%3D"', 428 | auth_header) 429 | 430 | def test_params_percent_encoding(self): 431 | params = 'oauth_body_hash=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=&oauth_consumer_key=abc-abc-abc!123&oa' \ 432 | 'uth_nonce=Wpe3LF09z1e3xQRI&oauth_signature_method=RSA-SHA256&oauth_timestamp=1626728330&oauth_versi' \ 433 | 'on=1.0¶m=token1:token2' 434 | encoded = util.percent_encode(params) 435 | self.assertEqual( 436 | 'oauth_body_hash%3D47DEQpj8HBSa%2B%2FTImW%2B5JCeuQeRkm5NMpJWZG3hSuFU%3D%26oauth_consumer_key%3Dabc-abc-' 437 | 'abc%21123%26oauth_nonce%3DWpe3LF09z1e3xQRI%26oauth_signature_method%3DRSA-SHA256%26oauth_timestamp%3D1626' 438 | '728330%26oauth_version%3D1.0%26param%3Dtoken1%3Atoken2', 439 | encoded) 440 | 441 | def test_sha256_encoding_when_no_str_or_byte(self): 442 | val = util.sha256_encode(123) 443 | self.assertEqual(b'\xa6e\xa4Y B/\x9dA~Hg\xef\xdcO\xb8\xa0J\x1f?\xff\x1f\xa0~\x99\x8e\x86\xf7\xf7\xa2z\xe3', val) 444 | 445 | def test_percent_encoding_of_None(self): 446 | self.assertEqual('', util.percent_encode(None)) 447 | 448 | def test_string_b64_encoding(self): 449 | t = util.base64_encode('foo bar foo bar') 450 | self.assertEqual('Zm9vIGJhciBmb28gYmFy', t) 451 | 452 | 453 | if __name__ == '__main__': 454 | unittest.main() 455 | -------------------------------------------------------------------------------- /tests/test_oauth_ext.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*-# 2 | # 3 | # 4 | # Copyright (c) 2020-2021 MasterCard International Incorporated 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without modification, are 8 | # permitted provided that the following conditions are met: 9 | # 10 | # Redistributions of source code must retain the above copyright notice, this list of 11 | # conditions and the following disclaimer. 12 | # Redistributions in binary form must reproduce the above copyright notice, this list of 13 | # conditions and the following disclaimer in the documentation and/or other materials 14 | # provided with the distribution. 15 | # Neither the name of the MasterCard International Incorporated nor the names of its 16 | # contributors may be used to endorse or promote products derived from this software 17 | # without specific prior written permission. 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 20 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 21 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 22 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 23 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 24 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 25 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 26 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 | # SUCH DAMAGE. 28 | # 29 | import unittest 30 | 31 | import hashlib 32 | import re 33 | from importlib import reload 34 | from unittest.mock import MagicMock 35 | 36 | from requests import PreparedRequest 37 | 38 | import oauth1.authenticationutils as authentication_utils 39 | from oauth1 import coreutils as util 40 | from oauth1.oauth import OAuth 41 | from oauth1.oauth_ext import OAuth1RSA 42 | 43 | 44 | class OAuthExtTest(unittest.TestCase): 45 | signing_key = authentication_utils.load_signing_key('./test_key_container.p12', "Password1") 46 | consumer_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 47 | data = 'sensistive data' 48 | mock_prepared_request = PreparedRequest() 49 | mock_prepared_request.prepare(headers={'Content-type': 'application/json', 'Accept': 'application/json'}, 50 | method="POST", 51 | url="http://www.example.com") 52 | 53 | def test_oauth_body_hash_with_body_string(self): 54 | oauth_object = OAuth1RSA(OAuthExtTest.consumer_key, OAuthExtTest.signing_key) 55 | OAuthExtTest.mock_prepared_request.body = "{'A' : 'sensitive data'}" 56 | 57 | oauth_object(OAuthExtTest.mock_prepared_request) 58 | h = OAuthExtTest.extract_oauth_params(OAuthExtTest.mock_prepared_request) 59 | 60 | self.assertEqual(h['oauth_body_hash'], 'sKrMRMpmhyMJ05fETctDp3UnlDsm1rgOJxQroerFuMs=') 61 | 62 | def test_oauth_body_hash_with_body_bytes(self): 63 | oauth_object = OAuth1RSA(OAuthExtTest.consumer_key, OAuthExtTest.signing_key) 64 | OAuthExtTest.mock_prepared_request.body = b'{"A" : OAuthExtTest.data}' 65 | 66 | oauth_object(OAuthExtTest.mock_prepared_request) 67 | h = OAuthExtTest.extract_oauth_params(OAuthExtTest.mock_prepared_request) 68 | 69 | hashlib_val = hashlib.sha256(OAuthExtTest.mock_prepared_request.body).digest() 70 | payload_hash_value = util.base64_encode(hashlib_val) 71 | 72 | self.assertEqual(h['oauth_body_hash'], '9MoCOjWt0ke+o8ZAGij+kZ1goHpfzLIG9ZGty05eIOo=') 73 | self.assertEqual(h['oauth_body_hash'], payload_hash_value) 74 | 75 | def test_oauth_body_hash_with_body_empty(self): 76 | oauth_object = OAuth1RSA(OAuthExtTest.consumer_key, OAuthExtTest.signing_key) 77 | OAuthExtTest.mock_prepared_request.body = '' 78 | 79 | oauth_object(OAuthExtTest.mock_prepared_request) 80 | h = OAuthExtTest.extract_oauth_params(OAuthExtTest.mock_prepared_request) 81 | 82 | hashlib_val = hashlib.sha256(str(OAuthExtTest.mock_prepared_request.body).encode('utf8')).digest() 83 | payload_hash_value = util.base64_encode(hashlib_val) 84 | 85 | self.assertEqual(h['oauth_body_hash'], payload_hash_value) 86 | self.assertEqual(h['oauth_body_hash'], '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=') 87 | 88 | def test_oauth_body_hash_with_body_none(self): 89 | oauth_object = OAuth1RSA(OAuthExtTest.consumer_key, OAuthExtTest.signing_key) 90 | OAuthExtTest.mock_prepared_request.body = None 91 | 92 | oauth_object(OAuthExtTest.mock_prepared_request) 93 | h = OAuthExtTest.extract_oauth_params(OAuthExtTest.mock_prepared_request) 94 | 95 | hashlib_val = hashlib.sha256(str("").encode('utf8')).digest() 96 | payload_hash_value = util.base64_encode(hashlib_val) 97 | 98 | self.assertEqual(h['oauth_body_hash'], payload_hash_value) 99 | 100 | def test_oauth_body_hash_with_body_empty_or_none(self): 101 | def prep_request(): 102 | req = PreparedRequest() 103 | req.prepare(headers={'Content-type': 'application/json', 'Accept': 'application/json'}, 104 | method="POST", 105 | url="http://www.example.com") 106 | return req 107 | 108 | oauth_object = OAuth1RSA(OAuthExtTest.consumer_key, OAuthExtTest.signing_key) 109 | request_empty = prep_request() 110 | request_none = prep_request() 111 | 112 | request_empty.body = "" 113 | request_none.body = None 114 | 115 | oauth_object(request_empty) 116 | request_empty_header = OAuthExtTest.extract_oauth_params(request_empty) 117 | 118 | oauth_object(request_none) 119 | request_none_header = OAuthExtTest.extract_oauth_params(request_none) 120 | 121 | empty_string_hash = hashlib.sha256(str("").encode('utf8')).digest() 122 | empty_string_encoded = util.base64_encode(empty_string_hash) 123 | 124 | self.assertEqual(request_empty_header['oauth_body_hash'], empty_string_encoded) 125 | self.assertEqual(request_empty_header['oauth_body_hash'], '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=') 126 | self.assertEqual(request_none_header['oauth_body_hash'], empty_string_encoded) 127 | self.assertEqual(request_none_header['oauth_body_hash'], '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=') 128 | 129 | def test_oauth_body_hash_with_body_multipart(self): 130 | oauth_object = OAuth1RSA(OAuthExtTest.consumer_key, OAuthExtTest.signing_key) 131 | mock_request = PreparedRequest() 132 | mock_request.prepare(headers={'Content-type': 'multipart/form-data'}, 133 | method="GET", 134 | url="http://www.mastercard.com") 135 | 136 | oauth_object(mock_request) 137 | h = OAuthExtTest.extract_oauth_params(mock_request) 138 | 139 | hashlib_val = hashlib.sha256(str(OAuthExtTest.mock_prepared_request.body).encode('utf8')).digest() 140 | payload_hash_value = util.base64_encode(hashlib_val) 141 | 142 | self.assertEqual(h['oauth_body_hash'], payload_hash_value) 143 | self.assertEqual(h['oauth_body_hash'], '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=') 144 | 145 | def test_call(self): 146 | oauth_object = OAuth1RSA(OAuthExtTest.consumer_key, OAuthExtTest.signing_key) 147 | call_object = oauth_object.__call__(OAuthExtTest.mock_prepared_request) 148 | self.assertTrue("Authorization" in call_object.headers) 149 | 150 | def test_ext_oauth_header_equals_to_non_ext_generated(self): 151 | util.get_nonce = MagicMock(return_value=util.get_nonce()) 152 | util.get_timestamp = MagicMock(return_value=util.get_timestamp()) 153 | 154 | oauth_object = OAuth1RSA(OAuthExtTest.consumer_key, OAuthExtTest.signing_key) 155 | call_object = oauth_object(OAuthExtTest.mock_prepared_request) 156 | 157 | header = OAuth.get_authorization_header(OAuthExtTest.mock_prepared_request.url, 158 | OAuthExtTest.mock_prepared_request.method, 159 | OAuthExtTest.mock_prepared_request.body, 160 | OAuthExtTest.consumer_key, OAuthExtTest.signing_key) 161 | reload(util) 162 | self.assertTrue("Authorization" in call_object.headers) 163 | self.assertEqual(header, call_object.headers['Authorization']) 164 | 165 | def test_with_none_arguments(self): 166 | oauth_object = OAuth1RSA(None, None) 167 | request = PreparedRequest() 168 | call_object = oauth_object(request) 169 | self.assertIsNone(call_object.headers) 170 | 171 | @staticmethod 172 | def to_pair(obj): 173 | split_index = obj.index('=') 174 | key = obj[:split_index] 175 | value = obj[split_index + 2:] 176 | return key, value[:-1] 177 | 178 | @staticmethod 179 | def extract_oauth_params(prepared_request: PreparedRequest): 180 | oauth_header = prepared_request.headers['Authorization'] 181 | h = str(re.sub(r'^OAuth ', '', oauth_header)) 182 | res = dict([OAuthExtTest.to_pair(item) for item in h.split(',')]) 183 | return res 184 | 185 | 186 | if __name__ == '__main__': 187 | unittest.main() 188 | -------------------------------------------------------------------------------- /tests/test_signer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*-# 2 | # 3 | # 4 | # Copyright 2019-2021 Mastercard 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without modification, are 8 | # permitted provided that the following conditions are met: 9 | # 10 | # Redistributions of source code must retain the above copyright notice, this list of 11 | # conditions and the following disclaimer. 12 | # Redistributions in binary form must reproduce the above copyright notice, this list of 13 | # conditions and the following disclaimer in the documentation and/or other materials 14 | # provided with the distribution. 15 | # Neither the name of the MasterCard International Incorporated nor the names of its 16 | # contributors may be used to endorse or promote products derived from this software 17 | # without specific prior written permission. 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 20 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 21 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 22 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 23 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 24 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 25 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 26 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 | # SUCH DAMAGE. 28 | # 29 | import unittest 30 | from unittest.mock import patch 31 | 32 | import requests 33 | from requests import Request, Session 34 | from requests.auth import AuthBase 35 | 36 | import oauth1.authenticationutils as authenticationutils 37 | from oauth1.signer import OAuthSigner 38 | 39 | 40 | class SignerTest(unittest.TestCase): 41 | signing_key = authenticationutils.load_signing_key('./test_key_container.p12', "Password1") 42 | consumer_key = 'dummy' 43 | uri = "https://sandbox.api.mastercard.com/fraud/merchant/v1/termination-inquiry?Format=XML&PageOffset=0" 44 | 45 | def test_sign_request(self): 46 | request = Request() 47 | request.method = "POST" 48 | request.data = "" 49 | 50 | signer = OAuthSigner(SignerTest.consumer_key, SignerTest.signing_key) 51 | request = signer.sign_request(SignerTest.uri, request) 52 | auth_header = request.headers['Authorization'] 53 | self.assertTrue("OAuth" in auth_header) 54 | self.assertTrue("dummy" in auth_header) 55 | 56 | @patch.object(Session, 'send') 57 | def test_sign_prepared_request(self, mock_send): 58 | class MCSigner(AuthBase): 59 | def __init__(self, consumer_key, signing_key): 60 | self.signer = OAuthSigner(consumer_key, signing_key) 61 | 62 | def __call__(self, request): 63 | self.signer.sign_request(request.url, request) 64 | return request 65 | 66 | signer = MCSigner(SignerTest.consumer_key, SignerTest.signing_key) 67 | requests.get(SignerTest.uri, auth=signer) 68 | 69 | auth_header = ( 70 | mock_send.call_args[0][0].headers if isinstance(mock_send.call_args, tuple) else mock_send.call_args.args 71 | [0].headers)['Authorization'] 72 | 73 | self.assertTrue("OAuth" in auth_header) 74 | self.assertTrue("oauth_consumer_key=\"dummy\"" in auth_header) 75 | 76 | 77 | if __name__ == '__main__': 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*-# 2 | # 3 | # 4 | # Copyright 2019-2021 Mastercard 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without modification, are 8 | # permitted provided that the following conditions are met: 9 | # 10 | # Redistributions of source code must retain the above copyright notice, this list of 11 | # conditions and the following disclaimer. 12 | # Redistributions in binary form must reproduce the above copyright notice, this list of 13 | # conditions and the following disclaimer in the documentation and/or other materials 14 | # provided with the distribution. 15 | # Neither the name of the MasterCard International Incorporated nor the names of its 16 | # contributors may be used to endorse or promote products derived from this software 17 | # without specific prior written permission. 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 20 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 21 | # SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 22 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 23 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 24 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 25 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 26 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 | # SUCH DAMAGE. 28 | # 29 | import unittest 30 | import oauth1.authenticationutils as authenticationutils 31 | from cryptography.hazmat.primitives import serialization 32 | 33 | 34 | class UtilsTest(unittest.TestCase): 35 | 36 | def test_load_signing_key_should_return_key(self): 37 | key_container_path = "./test_key_container.p12" 38 | key_password = "Password1" 39 | 40 | signing_key = authenticationutils.load_signing_key(key_container_path, key_password) 41 | self.assertTrue(signing_key.key_size) 42 | 43 | self.assertEqual(signing_key.key_size, 2048) 44 | 45 | private_key_bytes = signing_key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption()) 46 | self.assertTrue(private_key_bytes) 47 | 48 | 49 | if __name__ == '__main__': 50 | unittest.main() 51 | --------------------------------------------------------------------------------