Skip to content

Commit 867d386

Browse files
authored
use Vonage JWT generator instead of PyJWT for requests (#262)
* use Vonage JWT generator instead of PyJWT for requests * adding new test for multiple workflows in verify v2 (#263) * updating the changelog for new release * Bump version: 3.5.1 → 3.5.2 * updating the changelog for new release * internal refactoring to check for new Client._jwt_client object * removing superseded check * removing potentially misleading message
1 parent 896f990 commit 867d386

16 files changed

+163
-100
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 3.5.1
2+
current_version = 3.5.2
33
commit = True
44
tag = False
55

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 3.5.2
2+
- Using the [Vonage JWT Generator](https://github.com/Vonage/vonage-python-jwt) instead of `PyJWT` for generating JWTs.
3+
- Other internal refactoring and enhancements
4+
15
# 3.5.1
26
- Updating the internal use of the `fraud_check` parameter in the Vonage Verify V2 API
37

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ coverage:
55
coverage html
66

77
test:
8-
pytest -v
8+
pytest -vv --disable-warnings
99

1010
clean:
1111
rm -rf dist build

README.md

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
[![PyPI version](https://badge.fury.io/py/vonage.svg)](https://badge.fury.io/py/vonage)
66
[![Build Status](https://github.com/Vonage/vonage-python-sdk/workflows/Build/badge.svg)](https://github.com/Vonage/vonage-python-sdk/actions)
7-
[![codecov](https://codecov.io/gh/Vonage/vonage-python-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/Vonage/vonage-python-sdk)
87
[![Python versions supported](https://img.shields.io/pypi/pyversions/vonage.svg)](https://pypi.python.org/pypi/vonage)
98
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
109

@@ -728,34 +727,32 @@ your account before you can validate webhook signatures.
728727

729728
## JWT parameters
730729

731-
By default, the library generates short-lived tokens for JWT authentication.
730+
By default, the library generates 15-minute tokens for JWT authentication.
732731

733-
Use the auth method to specify parameters for a longer life token or to
734-
specify a different token identifier:
732+
Use the `auth` method of the client class to specify custom parameters:
735733

736734
```python
737735
client.auth(nbf=nbf, exp=exp, jti=jti)
736+
# OR
737+
client.auth({'nbf': nbf, 'exp': exp, 'jti': jti})
738738
```
739739

740740
## Overriding API Attributes
741741

742-
In order to rewrite/get the value of variables used across all the Vonage classes Python uses `Call by Object Reference` that allows you to create a single client for Sms/Voice Classes. This means that if you make a change on a client instance this will be available for the Sms class.
742+
In order to rewrite/get the value of variables used across all the Vonage classes Python uses `Call by Object Reference` that allows you to create a single client to use with all API classes.
743743

744744
An example using setters/getters with `Object references`:
745745

746746
```python
747-
from vonage import Client, Sms
747+
from vonage import Client
748748

749-
#Defines the client
749+
# Define the client
750750
client = Client(key='YOUR_API_KEY', secret='YOUR_API_SECRET')
751751
print(client.host()) # using getter for host -- value returned: rest.nexmo.com
752752

753-
#Define the sms instance
754-
sms = Sms(client)
755-
756-
#Change the value in client
757-
client.host('mio.nexmo.com') #Change host to mio.nexmo.com - this change will be available for sms
758-
753+
# Change the value in client
754+
client.host('mio.nexmo.com') # Change host to mio.nexmo.com - this change will be available for sms
755+
client.sms.send_message(params) # Sends an SMS to the host above
759756
```
760757

761758
### Overriding API Host / Host Attributes

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
setup(
1313
name="vonage",
14-
version="3.5.1",
14+
version="3.5.2",
1515
description="Vonage Server SDK for Python",
1616
long_description=long_description,
1717
long_description_content_type="text/markdown",
@@ -23,8 +23,8 @@
2323
package_dir={"": "src"},
2424
platforms=["any"],
2525
install_requires=[
26+
"vonage-jwt>=1.0.0",
2627
"requests>=2.4.2",
27-
"PyJWT[crypto]>=1.6.4",
2828
"pytz>=2018.5",
2929
"Deprecated",
3030
"pydantic>=1.10.2",

src/vonage/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .client import *
22
from .ncco_builder.ncco import *
33

4-
__version__ = "3.5.1"
4+
__version__ = "3.5.2"

src/vonage/_internal.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
from __future__ import annotations
2+
from typing import TYPE_CHECKING
3+
4+
if TYPE_CHECKING:
5+
from vonage import Client
6+
7+
18
def _format_date_param(params, key, format="%Y-%m-%d %H:%M:%S"):
29
"""
310
Utility function to convert datetime values to strings.
@@ -12,3 +19,10 @@ def _format_date_param(params, key, format="%Y-%m-%d %H:%M:%S"):
1219
param = params[key]
1320
if hasattr(param, "strftime"):
1421
params[key] = param.strftime(format)
22+
23+
24+
def set_auth_type(client: Client) -> str:
25+
if hasattr(client, '_jwt_client'):
26+
return 'jwt'
27+
else:
28+
return 'header'

src/vonage/client.py

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import vonage
2+
from vonage_jwt.jwt import JwtClient
23

34
from .account import Account
45
from .application import ApplicationV2, Application
@@ -20,11 +21,8 @@
2021
import base64
2122
import hashlib
2223
import hmac
23-
import jwt
2424
import os
2525
import time
26-
import re
27-
from uuid import uuid4
2826

2927
from requests import Response
3028
from requests.adapters import HTTPAdapter
@@ -95,16 +93,10 @@ def __init__(
9593
if self.signature_method in {"md5", "sha1", "sha256", "sha512"}:
9694
self.signature_method = getattr(hashlib, signature_method)
9795

98-
self._jwt_auth_params = {}
99-
10096
if private_key is not None and application_id is not None:
101-
self._application_id = application_id
102-
self._private_key = private_key
103-
104-
if isinstance(self._private_key, string_types) and re.search("[.][a-zA-Z0-9_]+$", self._private_key):
105-
with open(self._private_key, "rb") as key_file:
106-
self._private_key = key_file.read()
97+
self._jwt_client = JwtClient(application_id, private_key)
10798

99+
self._jwt_claims = {}
108100
self._host = "rest.nexmo.com"
109101
self._api_host = "api.nexmo.com"
110102

@@ -149,7 +141,7 @@ def api_host(self, value=None):
149141
self._api_host = value
150142

151143
def auth(self, params=None, **kwargs):
152-
self._jwt_auth_params = params or kwargs
144+
self._jwt_claims = params or kwargs
153145

154146
def check_signature(self, params):
155147
params = dict(params)
@@ -275,7 +267,7 @@ def delete(self, host, request_uri, auth_type=None):
275267
def parse(self, host, response: Response):
276268
logger.debug(f"Response headers {repr(response.headers)}")
277269
if response.status_code == 401:
278-
raise AuthenticationError("Authentication failed. Check you're using a valid authentication method.")
270+
raise AuthenticationError("Authentication failed.")
279271
elif response.status_code == 204:
280272
return None
281273
elif 200 <= response.status_code < 300:
@@ -312,21 +304,10 @@ def parse(self, host, response: Response):
312304
raise ServerError(message)
313305

314306
def _add_jwt_to_request_headers(self):
315-
return dict(self.headers, Authorization=b"Bearer " + self._generate_application_jwt())
316-
307+
return dict(
308+
self.headers,
309+
Authorization=b"Bearer " + self._generate_application_jwt()
310+
)
311+
317312
def _generate_application_jwt(self):
318-
iat = int(time.time())
319-
320-
payload = dict(self._jwt_auth_params)
321-
payload.setdefault("application_id", self._application_id)
322-
payload.setdefault("iat", iat)
323-
payload.setdefault("exp", iat + 60)
324-
payload.setdefault("jti", str(uuid4()))
325-
326-
token = jwt.encode(payload, self._private_key, algorithm="RS256")
327-
328-
# If token is string transform it to byte type
329-
if type(token) is str:
330-
token = bytes(token, 'utf-8')
331-
332-
return token
313+
return self._jwt_client.generate_application_jwt(self._jwt_claims)

src/vonage/messages.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ._internal import set_auth_type
12
from .errors import MessagesError
23

34
import re
@@ -15,13 +16,11 @@ class Messages:
1516

1617
def __init__(self, client):
1718
self._client = client
18-
self._auth_type = 'jwt'
19+
self._auth_type = set_auth_type(self._client)
1920

2021
def send_message(self, params: dict):
2122
self.validate_send_message_input(params)
2223

23-
if not hasattr(self._client, '_application_id'):
24-
self._auth_type = 'header'
2524
return self._client.post(
2625
self._client.api_host(),
2726
"/v1/messages",
@@ -40,7 +39,9 @@ def validate_send_message_input(self, params):
4039

4140
def _check_input_is_dict(self, params):
4241
if type(params) is not dict:
43-
raise MessagesError('Parameters to the send_message method must be specified as a dictionary.')
42+
raise MessagesError(
43+
'Parameters to the send_message method must be specified as a dictionary.'
44+
)
4445

4546
def _check_valid_message_channel(self, params):
4647
if params['channel'] not in Messages.valid_message_channels:
@@ -64,9 +65,13 @@ def _check_valid_recipient(self, params):
6465
if not isinstance(params['to'], str):
6566
raise MessagesError(f'Message recipient ("to={params["to"]}") not in a valid format.')
6667
elif params['channel'] != 'messenger' and not re.search(r'^[1-9]\d{6,14}$', params['to']):
67-
raise MessagesError(f'Message recipient number ("to={params["to"]}") not in a valid format.')
68+
raise MessagesError(
69+
f'Message recipient number ("to={params["to"]}") not in a valid format.'
70+
)
6871
elif params['channel'] == 'messenger' and not 0 < len(params['to']) < 50:
69-
raise MessagesError(f'Message recipient ID ("to={params["to"]}") not in a valid format.')
72+
raise MessagesError(
73+
f'Message recipient ID ("to={params["to"]}") not in a valid format.'
74+
)
7075

7176
def _check_valid_sender(self, params):
7277
if not isinstance(params['from'], str) or params['from'] == "":
@@ -76,8 +81,16 @@ def _check_valid_sender(self, params):
7681

7782
def _channel_specific_checks(self, params):
7883
if (
79-
(params['channel'] == 'whatsapp' and params['message_type'] == 'template' and 'whatsapp' not in params)
80-
or (params['channel'] == 'whatsapp' and params['message_type'] == 'sticker' and 'sticker' not in params)
84+
(
85+
params['channel'] == 'whatsapp'
86+
and params['message_type'] == 'template'
87+
and 'whatsapp' not in params
88+
)
89+
or (
90+
params['channel'] == 'whatsapp'
91+
and params['message_type'] == 'sticker'
92+
and 'sticker' not in params
93+
)
8194
or (params['channel'] == 'viber_service' and 'viber_service' not in params)
8295
):
8396
raise MessagesError(
@@ -95,4 +108,6 @@ def _check_valid_client_ref(self, params):
95108

96109
def _check_valid_whatsapp_sticker(self, sticker):
97110
if ('id' not in sticker and 'url' not in sticker) or ('id' in sticker and 'url' in sticker):
98-
raise MessagesError('Must specify one, and only one, of "id" or "url" in the "sticker" field.')
111+
raise MessagesError(
112+
'Must specify one, and only one, of "id" or "url" in the "sticker" field.'
113+
)

src/vonage/verify2.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
from __future__ import annotations
2+
from typing import TYPE_CHECKING
3+
4+
if TYPE_CHECKING:
5+
from vonage import Client
6+
17
from pydantic import BaseModel, ValidationError, validator, conint, constr
28
from typing import Optional, List
39

410
import copy
511
import re
612

13+
from ._internal import set_auth_type
714
from .errors import Verify2Error
815

916

@@ -17,9 +24,9 @@ class Verify2:
1724
'silent_auth',
1825
]
1926

20-
def __init__(self, client):
27+
def __init__(self, client: Client):
2128
self._client = client
22-
self._auth_type = 'jwt'
29+
self._auth_type = set_auth_type(self._client)
2330

2431
def new_request(self, params: dict):
2532
self._remove_unnecessary_fraud_check(params)
@@ -29,9 +36,6 @@ def new_request(self, params: dict):
2936
except (ValidationError, Verify2Error) as err:
3037
raise err
3138

32-
if not hasattr(self._client, '_application_id'):
33-
self._auth_type = 'header'
34-
3539
return self._client.post(
3640
self._client.api_host(),
3741
'/v2/verify',
@@ -42,9 +46,6 @@ def new_request(self, params: dict):
4246
def check_code(self, request_id: str, code: str):
4347
params = {'code': str(code)}
4448

45-
if not hasattr(self._client, '_application_id'):
46-
self._auth_type = 'header'
47-
4849
return self._client.post(
4950
self._client.api_host(),
5051
f'/v2/verify/{request_id}',
@@ -53,9 +54,6 @@ def check_code(self, request_id: str, code: str):
5354
)
5455

5556
def cancel_verification(self, request_id: str):
56-
if not hasattr(self._client, '_application_id'):
57-
self._auth_type = 'header'
58-
5957
return self._client.delete(
6058
self._client.api_host(),
6159
f'/v2/verify/{request_id}',
@@ -74,7 +72,9 @@ class VerifyRequest(BaseModel):
7472
client_ref: Optional[str]
7573
code_length: Optional[conint(ge=4, le=10)]
7674
fraud_check: Optional[bool]
77-
code: Optional[constr(min_length=4, max_length=10, regex='^(?=[a-zA-Z0-9]{4,10}$)[a-zA-Z0-9]*$')]
75+
code: Optional[
76+
constr(min_length=4, max_length=10, regex='^(?=[a-zA-Z0-9]{4,10}$)[a-zA-Z0-9]*$')
77+
]
7878

7979
@validator('workflow')
8080
def check_valid_workflow(cls, v):
@@ -95,7 +95,9 @@ def _check_valid_recipient(workflow):
9595
if 'to' not in workflow or (
9696
workflow['channel'] != 'email' and not re.search(r'^[1-9]\d{6,14}$', workflow['to'])
9797
):
98-
raise Verify2Error(f'You must specify a valid "to" value for channel "{workflow["channel"]}"')
98+
raise Verify2Error(
99+
f'You must specify a valid "to" value for channel "{workflow["channel"]}"'
100+
)
99101

100102
def _check_app_hash(workflow):
101103
if workflow['channel'] == 'sms' and 'app_hash' in workflow:
@@ -105,7 +107,9 @@ def _check_app_hash(workflow):
105107
it must be passed as a string and contain exactly 11 characters.'
106108
)
107109
elif workflow['channel'] != 'sms' and 'app_hash' in workflow:
108-
raise Verify2Error('Cannot specify a value for "app_hash" unless using SMS for authentication.')
110+
raise Verify2Error(
111+
'Cannot specify a value for "app_hash" unless using SMS for authentication.'
112+
)
109113

110114
def _check_whatsapp_sender(workflow):
111115
if not re.search(r'^[1-9]\d{6,14}$', workflow['from']):

0 commit comments

Comments
 (0)