Skip to content

Commit b73f096

Browse files
authored
Merge pull request #34 from Mastercard/feature/encode-oauth-signature
`oauth_signature` not encoded in versions 1.2.0 and 1.3.0
2 parents abd2a59 + 0bac0f8 commit b73f096

File tree

17 files changed

+531
-364
lines changed

17 files changed

+531
-364
lines changed

.github/workflows/lint.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Linter
2+
'on':
3+
push:
4+
branches:
5+
- main
6+
pull_request:
7+
branches:
8+
- main
9+
schedule:
10+
- cron: 0 16 * * *
11+
jobs:
12+
sonarcloud:
13+
name: Lint
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Python Style Checker
18+
uses: andymckay/[email protected]

.github/workflows/sonar.yml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ name: Sonar
33
push:
44
branches:
55
- main
6-
pull_request_target:
7-
types:
8-
- opened
9-
- synchronize
10-
- reopened
6+
pull_request:
7+
branches:
8+
- main
119
schedule:
1210
- cron: 0 16 * * *
1311
jobs:
@@ -35,4 +33,4 @@ jobs:
3533
uses: SonarSource/sonarcloud-github-action@master
3634
env:
3735
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
38-
SONAR_TOKEN: '${{ secrets.SONAR_TOKEN }}'
36+
SONAR_TOKEN: '${{ secrets.SONAR_TOKEN }}'

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,20 @@ signing_key = authenticationutils.load_signing_key('<insert PKCS#12 key file pat
6060
```
6161

6262
### Creating the OAuth Authorization Header <a name="creating-the-oauth-authorization-header"></a>
63-
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.
63+
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.
6464

6565
#### POST example
6666

6767
```python
6868
uri = 'https://sandbox.api.mastercard.com/service'
6969
payload = 'Hello world!'
70-
authHeader = OAuth().get_authorization_header(uri, 'POST', payload, '<insert consumer key>', signing_key)
70+
authHeader = OAuth.get_authorization_header(uri, 'POST', payload, '<insert consumer key>', signing_key)
7171
```
7272

7373
#### GET example
7474
```python
7575
uri = 'https://sandbox.api.mastercard.com/service'
76-
authHeader = OAuth().get_authorization_header(uri, 'GET', None, '<insert consumer key>', signing_key)
76+
authHeader = OAuth.get_authorization_header(uri, 'GET', None, '<insert consumer key>', signing_key)
7777
```
7878

7979
#### Use of authHeader with requests module (POST and GET example)

oauth1/authenticationutils.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
#!/usr/bin/env python
2-
# -*- coding: utf-8 -*-
1+
# -*- coding: utf-8 -*-
32
#
4-
# Copyright 2019-2020 Mastercard
3+
# Copyright 2019-2021 Mastercard
54
# All rights reserved.
65
#
76
# Redistribution and use in source and binary forms, with or without modification, are
@@ -28,10 +27,10 @@
2827
#
2928
from OpenSSL import crypto
3029

30+
3131
def load_signing_key(pkcs12_filename, password):
3232
private_key_file = open(pkcs12_filename, 'rb')
3333
p12 = crypto.load_pkcs12(private_key_file.read(), password.encode("utf-8"))
3434
private_key = p12.get_privatekey()
3535
private_key_file.close()
3636
return private_key
37-

oauth1/coreutils.py

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
#!/usr/bin/env python
2-
# -*- coding: utf-8 -*-
1+
# -*- coding: utf-8 -*-
32
#
4-
# Copyright 2019-2020 Mastercard
3+
# Copyright 2019-2021 Mastercard
54
# All rights reserved.
65
#
76
# Redistribution and use in source and binary forms, with or without modification, are
@@ -31,8 +30,12 @@
3130
"""
3231
import hashlib
3332
import base64
33+
import urllib
34+
import time
35+
from random import SystemRandom
36+
37+
from urllib.parse import urlparse, parse_qsl
3438

35-
from urllib.parse import urlparse, quote, parse_qsl
3639

3740
def normalize_params(url, params):
3841
"""
@@ -44,36 +47,26 @@ def normalize_params(url, params):
4447

4548
# Get the query list
4649
qs_list = parse_qsl(parse.query, keep_blank_values=True)
50+
must_encode = False if parse.query == urllib.parse.unquote(parse.query) else True
4751
if params is None:
4852
combined_list = qs_list
4953
else:
50-
combined_list = list(qs_list)
54+
# Needs to be encoded before sorting
55+
combined_list = [encode_pair(must_encode, key, value) for (key, value) in list(qs_list)]
5156
combined_list += params.items()
5257

53-
# Needs to be encoded before sorting
54-
encoded_list = [encode_pair(key, value) for (key, value) in combined_list]
55-
sorted_list = sorted(encoded_list, key=lambda x:x)
58+
encoded_list = ["%s=%s" % (key, value) for (key, value) in combined_list]
59+
sorted_list = sorted(encoded_list, key=lambda x: x)
5660

5761
return "&".join(sorted_list)
5862

5963

60-
def encode_pair(key, value):
61-
encoded_key = oauth_query_string_element_encode(key)
62-
encoded_value = oauth_query_string_element_encode(value if isinstance(value, bytes) else str(value))
63-
return "%s=%s" % (encoded_key, encoded_value)
64-
65-
def oauth_query_string_element_encode(value):
66-
"""
67-
RFC 3986 encodes the value
64+
def encode_pair(must_encode, key, value):
65+
encoded_key = percent_encode(key) if must_encode else key.replace(' ', '+')
66+
value = value if isinstance(value, bytes) else str(value)
67+
encoded_value = percent_encode(value) if must_encode else value.replace(' ', '+')
68+
return encoded_key, encoded_value
6869

69-
Note. This is based on RFC3986 but according to https://tools.ietf.org/html/rfc5849#section-3.6
70-
it replaces space with %20 not "+".
71-
"""
72-
encoded = quote(value)
73-
encoded = str.replace(encoded, ':', '%3A')
74-
encoded = str.replace(encoded, '+', '%2B')
75-
encoded = str.replace(encoded, '*', '%2A')
76-
return encoded
7770

7871
def normalize_url(url):
7972
"""
@@ -83,40 +76,69 @@ def normalize_url(url):
8376

8477
# netloc should be lowercase
8578
netloc = parse.netloc.lower()
86-
if parse.scheme=="http":
79+
if parse.scheme == "http":
8780
if netloc.endswith(":80"):
8881
netloc = netloc[:-3]
8982

90-
elif parse.scheme=="https" and netloc.endswith(":443"):
83+
elif parse.scheme == "https" and netloc.endswith(":443"):
9184
netloc = netloc[:-4]
9285

9386
# add a '/' at the end of the netloc if there in no path
9487
if not parse.path:
95-
netloc = netloc+"/"
88+
netloc = netloc + "/"
9689

9790
return "{}://{}{}".format(parse.scheme, netloc, parse.path)
9891

9992

100-
def uri_rfc3986_encode(value):
101-
"""
102-
RFC 3986 encodes the value
103-
"""
104-
return quote(value, safe='%')
105-
106-
10793
def sha256_encode(text):
10894
"""
10995
Returns the digest of SHA-256 of the text
11096
"""
111-
return hashlib.sha256(str(text).encode('utf-8')).digest()
97+
_hash = hashlib.sha256
98+
if type(text) is str:
99+
return _hash(text.encode('utf8')).digest()
100+
elif type(text) is bytes:
101+
return _hash(text).digest()
102+
elif not text:
103+
# Generally for calls where the payload is empty. Eg: get calls
104+
# Fix for AttributeError: 'NoneType' object has no attribute 'encode'
105+
return _hash("".encode('utf8')).digest()
106+
else:
107+
return _hash(str(text).encode('utf-8')).digest()
112108

113109

114110
def base64_encode(text):
115111
"""
116112
Base64 encodes the given input
117113
"""
114+
if not isinstance(text, (bytes, bytearray)):
115+
text = bytes(text.encode())
118116
encode = base64.b64encode(text)
119-
if isinstance(encode, (bytearray, bytes)):
120-
return encode.decode('ascii')
121-
else:
122-
return encode
117+
return encode.decode('ascii')
118+
119+
120+
def percent_encode(text):
121+
"""
122+
Percent encodes a string as per https://tools.ietf.org/html/rfc3986
123+
"""
124+
if text is None:
125+
return ''
126+
text = text.encode('utf-8') if isinstance(text, str) else text
127+
text = urllib.parse.quote(text, safe=b'~')
128+
return text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
129+
130+
131+
def get_nonce(length=16):
132+
"""
133+
Returns a random string of length=@length
134+
"""
135+
characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
136+
charlen = len(characters)
137+
return "".join([characters[SystemRandom().randint(0, charlen - 1)] for _ in range(0, length)])
138+
139+
140+
def get_timestamp():
141+
"""
142+
Returns the UTC timestamp (seconds passed since epoch)
143+
"""
144+
return int(time.time())

0 commit comments

Comments
 (0)