Skip to content

Commit

Permalink
use Vonage JWT generator instead of PyJWT for requests
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkahan committed Jun 2, 2023
1 parent 896f990 commit ff493fc
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 65 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ coverage:
coverage html

test:
pytest -v
pytest -vv --disable-warnings

clean:
rm -rf dist build
Expand Down
23 changes: 10 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

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

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

## JWT parameters

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

Use the auth method to specify parameters for a longer life token or to
specify a different token identifier:
Use the `auth` method of the client class to specify custom parameters:

```python
client.auth(nbf=nbf, exp=exp, jti=jti)
# OR
client.auth({'nbf': nbf, 'exp': exp, 'jti': jti})
```

## Overriding API Attributes

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.
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.

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

```python
from vonage import Client, Sms
from vonage import Client

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

#Define the sms instance
sms = Sms(client)

#Change the value in client
client.host('mio.nexmo.com') #Change host to mio.nexmo.com - this change will be available for sms

# Change the value in client
client.host('mio.nexmo.com') # Change host to mio.nexmo.com - this change will be available for sms
client.sms.send_message(params) # Sends an SMS to the host above
```

### Overriding API Host / Host Attributes
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
package_dir={"": "src"},
platforms=["any"],
install_requires=[
"vonage-jwt>=1.0.0",
"requests>=2.4.2",
"PyJWT[crypto]>=1.6.4",
"pytz>=2018.5",
"Deprecated",
"pydantic>=1.10.2",
Expand Down
39 changes: 10 additions & 29 deletions src/vonage/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import vonage
from vonage_jwt.jwt import JwtClient

from .account import Account
from .application import ApplicationV2, Application
Expand All @@ -20,11 +21,8 @@
import base64
import hashlib
import hmac
import jwt
import os
import time
import re
from uuid import uuid4

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

self._jwt_auth_params = {}

if private_key is not None and application_id is not None:
self._application_id = application_id
self._private_key = private_key

if isinstance(self._private_key, string_types) and re.search("[.][a-zA-Z0-9_]+$", self._private_key):
with open(self._private_key, "rb") as key_file:
self._private_key = key_file.read()
self._jwt_client = JwtClient(application_id, private_key)

self._jwt_claims = {}
self._host = "rest.nexmo.com"
self._api_host = "api.nexmo.com"

Expand Down Expand Up @@ -149,7 +141,7 @@ def api_host(self, value=None):
self._api_host = value

def auth(self, params=None, **kwargs):
self._jwt_auth_params = params or kwargs
self._jwt_claims = params or kwargs

def check_signature(self, params):
params = dict(params)
Expand Down Expand Up @@ -312,21 +304,10 @@ def parse(self, host, response: Response):
raise ServerError(message)

def _add_jwt_to_request_headers(self):
return dict(self.headers, Authorization=b"Bearer " + self._generate_application_jwt())

return dict(
self.headers,
Authorization=b"Bearer " + self._generate_application_jwt()
)

def _generate_application_jwt(self):
iat = int(time.time())

payload = dict(self._jwt_auth_params)
payload.setdefault("application_id", self._application_id)
payload.setdefault("iat", iat)
payload.setdefault("exp", iat + 60)
payload.setdefault("jti", str(uuid4()))

token = jwt.encode(payload, self._private_key, algorithm="RS256")

# If token is string transform it to byte type
if type(token) is str:
token = bytes(token, 'utf-8')

return token
return self._jwt_client.generate_application_jwt(self._jwt_claims)
16 changes: 2 additions & 14 deletions tests/test_getters_setters.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
from util import *

@responses.activate
def test_getters(client, dummy_data):
assert client.host() == dummy_data.host
assert client.api_host() == dummy_data.api_host

@responses.activate
def test_setters(client, dummy_data):
try:
client.host('host.nexmo.com')
client.api_host('host.nexmo.com')
client.host('host.vonage.com')
client.api_host('host.vonage.com')
assert client.host() != dummy_data.host
assert client.api_host() != dummy_data.api_host
except:
assert False

@responses.activate
def test_fail_setter_url_format(client, dummy_data):
try:
client.host('1000.1000')
assert False
except:
assert True
39 changes: 39 additions & 0 deletions tests/test_jwt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from time import time
from unittest.mock import patch

now = int(time())


def test_auth_sets_claims_from_kwargs(client):
client.auth(jti='asdfzxcv1234', nbf=now + 100, exp=now + 1000)
assert client._jwt_claims['jti'] == 'asdfzxcv1234'
assert client._jwt_claims['nbf'] == now + 100
assert client._jwt_claims['exp'] == now + 1000


def test_auth_sets_claims_from_dict(client):
custom_jwt_claims = {'jti': 'asdfzxcv1234', 'nbf': now + 100, 'exp': now + 1000}
client.auth(custom_jwt_claims)
assert client._jwt_claims['jti'] == 'asdfzxcv1234'
assert client._jwt_claims['nbf'] == now + 100
assert client._jwt_claims['exp'] == now + 1000


test_jwt = b'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBsaWNhdGlvbl9pZCI6ImFzZGYxMjM0IiwiaWF0IjoxNjg1NzMxMzkxLCJqdGkiOiIwYzE1MDJhZS05YmI5LTQ4YzQtYmQyZC0yOGFhNWUxYjZkMTkiLCJleHAiOjE2ODU3MzIyOTF9.mAkGeVgWOb7Mrzka7DSj32vSM8RaFpYse_2E7jCQ4DuH8i32wq9FxXGgfwdBQDHzgku3RYIjLM1xlVrGjNM3MsnZgR7ymQ6S4bdTTOmSK0dKbk91SrN7ZAC9k2a6JpCC2ZYgXpZ5BzpDTdy9BYu6msHKmkL79_aabFAhrH36Nk26pLvoI0-KiGImEex-aRR4iiaXhOebXBeqiQTRPKoKizREq4-8zBQv_j6yy4AiEYvBatQ8L_sjHsLj9jjITreX8WRvEW-G4TPpPLMaHACHTDMpJSOZAnegAkzTV2frVRmk6DyVXnemm4L0RQD1XZDaH7JPsKk24Hd2WZQyIgHOqQ'


def vonage_jwt_mock(self, claims):
return test_jwt


def test_generate_application_jwt(client):
with patch('vonage.client.JwtClient.generate_application_jwt', vonage_jwt_mock):
jwt = client._generate_application_jwt()
assert jwt == test_jwt


def test_add_jwt_to_request_headers(client):
with patch('vonage.client.JwtClient.generate_application_jwt', vonage_jwt_mock):
headers = client._add_jwt_to_request_headers()
assert headers['Accept'] == 'application/json'
assert headers['Authorization'] == b'Bearer ' + test_jwt
9 changes: 9 additions & 0 deletions tests/test_rest_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,12 @@ def test_delete_with_header_auth(client, dummy_data):
assert isinstance(response, dict)
assert request_user_agent() == dummy_data.user_agent
assert_basic_auth()

@responses.activate
def test_get_with_jwt_auth(client, dummy_data):
stub(responses.GET, "https://api.nexmo.com/v1/calls")
host = "api.nexmo.com"
request_uri = "/v1/calls"
response = client.get(host, request_uri, auth_type='jwt')
assert isinstance(response, dict)
assert request_user_agent() == dummy_data.user_agent
15 changes: 8 additions & 7 deletions tests/test_voice.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,21 +139,22 @@ def test_send_dtmf(voice, dummy_data):


@responses.activate
def test_user_provided_authorization(client, dummy_data):
def test_user_provided_authorization(dummy_data):
stub(responses.GET, "https://api.nexmo.com/v1/calls/xx-xx-xx-xx")

application_id = "different-nexmo-application-id"
application_id = "different-application-id"
client = vonage.Client(application_id=application_id, private_key=dummy_data.private_key)

nbf = int(time.time())
exp = nbf + 3600

client.auth(application_id=application_id, nbf=nbf, exp=exp)
voice = vonage.Voice(client)
voice.get_call("xx-xx-xx-xx")

client.auth(nbf=nbf, exp=exp)
client.voice.get_call("xx-xx-xx-xx")

token = request_authorization().split()[1]

token = jwt.decode(token, dummy_data.public_key, algorithms="RS256")

print(token)
assert token["application_id"] == application_id
assert token["nbf"] == nbf
assert token["exp"] == exp
Expand Down

0 comments on commit ff493fc

Please sign in to comment.