├── .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/)
3 |
4 | [](https://github.com/Mastercard/oauth1-signer-python/actions?query=workflow%3A%22Build+%26+Test%22)
5 | [](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-python)
6 | [](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-python)
7 | [](https://sonarcloud.io/dashboard?id=Mastercard_oauth1-signer-python)
8 | [](https://github.com/Mastercard/oauth1-signer-python/actions?query=workflow%3A%22broken+links%3F%22)
9 | [](https://pypi.org/project/mastercard-oauth1-signer)
10 | [](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 = "19961TESTTEST555555555512345678905555 Test " \
181 | "LaneTESTXX12345USAJohnSmith123456789055555555555555 Test " \
184 | "LaneTESTXX12345USA1234567890XX" \
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 |
--------------------------------------------------------------------------------