Skip to content

Commit 06f319d

Browse files
authored
Merge pull request #29 from Akshaykdubey/master
Adding oauth ext lib and tests
2 parents 1738065 + abfb462 commit 06f319d

File tree

5 files changed

+367
-16
lines changed

5 files changed

+367
-16
lines changed

README.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,47 @@ signer = OAuthSigner(consumer_key, signing_key)
107107
request = signer.sign_request(uri, request)
108108
```
109109

110+
111+
#### Usage of the `oauth_ext`
112+
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:
113+
114+
###### POST example
115+
116+
```python
117+
from oauth1.oauth_ext import OAuth1RSA
118+
from oauth1.oauth_ext import HASH_SHA256
119+
import requests
120+
121+
uri = 'https://sandbox.api.mastercard.com/service'
122+
oauth = OAuth1RSA(consumer_key, signing_key)
123+
header = {'Content-type' : 'application/json', 'Accept' : 'application/json'}
124+
125+
# Passing payload for data parameter as string
126+
payload = '{"key" : "value"}'
127+
request = requests.post(uri, data=payload, auth=oauth, headers=header)
128+
129+
# Passing payload for data parameter as Json object
130+
payload = {'key' : 'value'}
131+
request = requests.post(uri, data=json.dumps(payload), auth=oauth, headers=header)
132+
133+
# Passing payload for json parameter Json object
134+
payload = {'key' : 'value'}
135+
request = requests.post(uri, json=payload, auth=oauth, headers=header)
136+
```
137+
138+
###### GET example
139+
140+
```python
141+
from oauth1.oauth_ext import OAuth1RSA
142+
import requests
143+
144+
uri = 'https://sandbox.api.mastercard.com/service'
145+
oauth = OAuth1RSA(consumer_key, signing_key)
146+
147+
# Operation for get call
148+
request = requests.get(uri, auth=oauth)
149+
```
150+
110151
### Integrating with OpenAPI Generator API Client Libraries <a name="integrating-with-openapi-generator-api-client-libraries"></a>
111152

112153
[OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) generates API client libraries from [OpenAPI Specs](https://github.com/OAI/OpenAPI-Specification).
@@ -125,7 +166,7 @@ Client libraries can be generated using the following command:
125166
```shell
126167
java -jar openapi-generator-cli.jar generate -i openapi-spec.yaml -g python -o out
127168
```
128-
See also:
169+
See also:
129170
* [OpenAPI Generator (executable)](https://mvnrepository.com/artifact/org.openapitools/openapi-generator-cli)
130171
* [CONFIG OPTIONS for python](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/python.md)
131172

oauth1/oauth_ext.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright 2020-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 time
30+
from uuid import uuid4
31+
32+
from requests.auth import AuthBase
33+
from OpenSSL.crypto import PKey
34+
from OpenSSL import crypto
35+
from requests import PreparedRequest
36+
import hashlib
37+
from . import coreutils as util
38+
39+
HASH_SHA256 = 'SHA256'
40+
41+
42+
def hash_func(hash_alg):
43+
return {
44+
HASH_SHA256: hashlib.sha256
45+
}[hash_alg]
46+
47+
48+
class OAuth1RSA(AuthBase):
49+
"""OAuth1 RSA-SHA256 requests's auth helper
50+
Usage:
51+
>>> from oauth1 import authenticationutils
52+
>>> from oauth1.auth_ext import OAuth1RSA
53+
>>> import requests
54+
>>> CONSUMER_KEY = 'secret-consumer-key'
55+
>>> pk = authenticationutils.load_signing_key('instance/masterpass.pfx', 'a3fa02536a')
56+
>>> oauth = OAuth1RSA(CONSUMER_KEY, pk)
57+
>>> requests.post('https://endpoint.com/the/route', data={'foo': 'bar'}, auth=oauth)
58+
"""
59+
60+
def __init__(self, consumer_key: str, signing_key: PKey):
61+
self.consumer_key = consumer_key
62+
self.signing_key = signing_key
63+
self.hash_alg = HASH_SHA256
64+
self.hash_f = hash_func(HASH_SHA256)
65+
66+
def __call__(self, r: PreparedRequest):
67+
payload = {
68+
'oauth_version': '1.0',
69+
'oauth_nonce': self.nonce(),
70+
'oauth_timestamp': str(self.timestamp()),
71+
'oauth_signature_method': f'RSA-{self.hash_alg}',
72+
'oauth_consumer_key': self.consumer_key
73+
}
74+
75+
# Google's body hash extension
76+
payload = self.oauth_body_hash(r, payload)
77+
78+
signable_message = self.signable_message(r, payload)
79+
signature = self.signature(signable_message)
80+
payload['oauth_signature'] = signature
81+
82+
h = self._generate_header(payload)
83+
84+
r.headers['Authorization'] = h
85+
return r
86+
87+
@staticmethod
88+
def nonce():
89+
return str(uuid4())
90+
91+
@staticmethod
92+
def timestamp():
93+
return int(time.time())
94+
95+
def _hash(self, message: str) -> str:
96+
if type(message) is str:
97+
return self.hash_f(message.encode('utf8')).digest()
98+
elif type(message) is bytes:
99+
return self.hash_f(message).digest()
100+
else:
101+
# Generally for calls where the payload is empty. Eg: get calls
102+
# Fix for AttributeError: 'NoneType' object has no attribute 'encode'
103+
return self.hash_f(str(message).encode('utf8')).digest()
104+
105+
@staticmethod
106+
def signable_message(r: PreparedRequest, payload: dict):
107+
params = [
108+
r.method.upper(),
109+
util.normalize_url(r.url),
110+
util.normalize_params(r.url, payload)
111+
]
112+
params = map(util.uri_rfc3986_encode, params)
113+
return '&'.join(params)
114+
115+
def signature(self, message: str):
116+
signature = crypto.sign(self.signing_key, message, self.hash_alg)
117+
return util.base64_encode(signature)
118+
119+
@staticmethod
120+
def _generate_header(payload: dict):
121+
_ = util.uri_rfc3986_encode
122+
pts = [f'{_(k)}="{_(v)}"' for k, v in sorted(payload.items())]
123+
msg = ','.join(pts)
124+
return f'OAuth {msg}'
125+
126+
def oauth_body_hash(self, r: PreparedRequest, payload: dict):
127+
if r.headers and r.headers.get('content-type') == 'multipart/form-data':
128+
return payload
129+
130+
body = r.body
131+
payload['oauth_body_hash'] = util.uri_rfc3986_encode(util.base64_encode(self._hash(body)))
132+
return payload

tests/test_oauth.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,19 @@
3737
class OAuthTest(unittest.TestCase):
3838

3939
signing_key = authenticationutils.load_signing_key('./test_key_container.p12', "Password1")
40+
uri = 'https://www.example.com'
4041

4142
def test_get_authorization_header_nominal(self):
42-
header = OAuth().get_authorization_header('https://www.example.com', 'POST', 'payload', 'dummy', OAuthTest.signing_key)
43+
header = OAuth().get_authorization_header(OAuthTest.uri, 'POST', 'payload', 'dummy', OAuthTest.signing_key)
4344
self.assertTrue("OAuth" in header)
4445
self.assertTrue("dummy" in header)
4546

4647
def test_get_authorization_header_should_compute_body_hash(self):
47-
header = OAuth().get_authorization_header('https://www.example.com', 'POST', '{}', 'dummy', OAuthTest.signing_key)
48+
header = OAuth().get_authorization_header(OAuthTest.uri, 'POST', '{}', 'dummy', OAuthTest.signing_key)
4849
self.assertTrue('RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' in header)
4950

5051
def test_get_authorization_header_should_return_empty_string_body_hash(self):
51-
header = OAuth().get_authorization_header('https://www.example.com', 'GET', None, 'dummy', OAuthTest.signing_key)
52+
header = OAuth().get_authorization_header(OAuthTest.uri, 'GET', None, 'dummy', OAuthTest.signing_key)
5253
self.assertTrue('47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' in header)
5354

5455
def test_get_nonce(self):
@@ -60,8 +61,8 @@ def test_get_timestamp(self):
6061
self.assertEqual(len(str(timestamp)),10)
6162

6263
def test_sign_message(self):
63-
baseString = 'POST&https%3A%2F%2Fsandbox.api.mastercard.com%2Ffraud%2Fmerchant%2Fv1%2Ftermination-inquiry&Format%3DXML%26PageLength%3D10%26PageOffset%3D0%26oauth_body_hash%3DWhqqH%252BTU95VgZMItpdq78BWb4cE%253D%26oauth_consumer_key%3Dxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx%26oauth_nonce%3D1111111111111111111%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1111111111%26oauth_version%3D1.0'
64-
signature = OAuth().sign_message(baseString, OAuthTest.signing_key)
64+
base_string = 'POST&https%3A%2F%2Fsandbox.api.mastercard.com%2Ffraud%2Fmerchant%2Fv1%2Ftermination-inquiry&Format%3DXML%26PageLength%3D10%26PageOffset%3D0%26oauth_body_hash%3DWhqqH%252BTU95VgZMItpdq78BWb4cE%253D%26oauth_consumer_key%3Dxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx%26oauth_nonce%3D1111111111111111111%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1111111111%26oauth_version%3D1.0'
65+
signature = OAuth().sign_message(base_string, OAuthTest.signing_key)
6566
signature = Util.uri_rfc3986_encode(signature)
6667
self.assertEqual(signature,"DvyS3R795sUb%2FcvBfiFYZzPDU%2BRVefW6X%2BAfyu%2B9fxjudQft%2BShXhpounzJxYCwOkkjZWXOR0ICTMn6MOuG04TTtmPMrOxj5feGwD3leMBsi%2B3XxcFLPi8BhZKqgapcAqlGfjEhq0COZ%2FF9aYDcjswLu0zgrTMSTp4cqXYMr9mbQVB4HL%2FjiHni5ejQu9f6JB9wWW%2BLXYhe8F6b4niETtzIe5o77%2B%2BkKK67v9wFIZ9pgREz7ug8K5DlxX0DuwdUKFhsenA5z%2FNNCZrJE%2BtLu0tSjuF5Gsjw5GRrvW33MSoZ0AYfeleh5V3nLGgHrhVjl5%2BiS40pnG2po%2F5hIAUT5ag%3D%3D")
6768

@@ -95,7 +96,7 @@ def test_nonce_length(self):
9596
def test_nonce_uniqueness(self):
9697
init = OAuth.get_nonce(self)
9798
l = []
98-
for i in range(0,100000):
99+
for _ in range(0,100000):
99100
l.append(init + OAuth.get_nonce(self))
100101

101102
self.assertEqual(self.list_duplicates(l), [])
@@ -168,10 +169,6 @@ def test_signature_base_string2(self):
168169
encoded_hash = Util.base64_encode(Util.sha256_encode(body))
169170
oauth_parameters.set_oauth_body_hash(encoded_hash)
170171

171-
oauth_parameters_base = oauth_parameters.get_base_parameters_dict()
172-
merge_parameters = oauth_parameters_base.copy()
173-
174-
query_params = Util.normalize_params(url, merge_parameters)
175172
base_string = OAuth.get_base_string(self, url, method, oauth_parameters.get_base_parameters_dict())
176173
expected = "POST&https%3A%2F%2Fsandbox.api.mastercard.com%2Ffraud%2Fmerchant%2Fv1%2Ftermination-inquiry&Format%3DXML%26PageLength%3D10%26PageOffset%3D0%26oauth_body_hash%3Dh2Pd7zlzEZjZVIKB4j94UZn%2FxxoR3RoCjYQ9%2FJdadGQ%3D%26oauth_consumer_key%3Dxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx%26oauth_nonce%3D1111111111111111111%26oauth_timestamp%3D1111111111%26oauth_version%3D1.0"
177174

@@ -181,9 +178,9 @@ def test_sign_signature_base_string_invalid_key(self):
181178
self.assertRaises(AttributeError, OAuth.sign_message, self, "some string", None)
182179

183180
def test_sign_signature_base_string(self):
184-
expectedSignatureString = "IJeNKYGfUhFtj5OAPRI92uwfjJJLCej3RCMLbp7R6OIYJhtwxnTkloHQ2bgV7fks4GT/A7rkqrgUGk0ewbwIC6nS3piJHyKVc7rvQXZuCQeeeQpFzLRiH3rsb+ZS+AULK+jzDje4Fb+BQR6XmxuuJmY6YrAKkj13Ln4K6bZJlSxOizbNvt+Htnx+hNd4VgaVBeJKcLhHfZbWQxK76nMnjY7nDcM/2R6LUIR2oLG1L9m55WP3bakAvmOr392ulv1+mWCwDAZZzQ4lakDD2BTu0ZaVsvBW+mcKFxYeTq7SyTQMM4lEwFPJ6RLc8jJJ+veJXHekLVzWg4qHRtzNBLz1mA=="
181+
expected_signature_string = "IJeNKYGfUhFtj5OAPRI92uwfjJJLCej3RCMLbp7R6OIYJhtwxnTkloHQ2bgV7fks4GT/A7rkqrgUGk0ewbwIC6nS3piJHyKVc7rvQXZuCQeeeQpFzLRiH3rsb+ZS+AULK+jzDje4Fb+BQR6XmxuuJmY6YrAKkj13Ln4K6bZJlSxOizbNvt+Htnx+hNd4VgaVBeJKcLhHfZbWQxK76nMnjY7nDcM/2R6LUIR2oLG1L9m55WP3bakAvmOr392ulv1+mWCwDAZZzQ4lakDD2BTu0ZaVsvBW+mcKFxYeTq7SyTQMM4lEwFPJ6RLc8jJJ+veJXHekLVzWg4qHRtzNBLz1mA=="
185182
signing_string = OAuth.sign_message(self, "baseString", OAuthTest.signing_key)
186-
self.assertEqual(expectedSignatureString, signing_string)
183+
self.assertEqual(expected_signature_string, signing_string)
187184

188185
def test_url_normalization_rfc_examples1(self):
189186
uri = "https://www.example.net:8080"

0 commit comments

Comments
 (0)