-
Notifications
You must be signed in to change notification settings - Fork 115
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
300 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# 1.1.0 | ||
- Add new module with method to verify JWT signatures, `verify_jwt.verify_signature` | ||
|
||
# 1.0.0 | ||
- First stable release |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Vonage JWT Generator for Python | ||
|
||
This package (`vonage-jwt`) provides functionality to generate a JWT in Python code. | ||
|
||
It is used by the [Vonage Python SDK](https://github.com/Vonage/vonage-python-sdk), specifically by the `vonage-http-client` package, to generate JWTs for authentication. Thus, it doesn't require manual installation or configuration unless you're using this package independently of a SDK. | ||
|
||
For full API documentation, refer to the [Vonage Developer documentation](https://developer.vonage.com). | ||
|
||
- [Installation](#installation) | ||
- [Generating JWTs](#generating-jwts) | ||
- [Verifying a JWT signature](#verifying-a-jwt-signature) | ||
|
||
## Installation | ||
|
||
Install from the Python Package Index with pip: | ||
|
||
```bash | ||
pip install vonage-jwt | ||
``` | ||
|
||
## Generating JWTs | ||
|
||
This JWT Generator can be used implicitly, just by using the [Vonage Python SDK](https://github.com/Vonage/vonage-python-sdk) to make JWT-authenticated API calls. | ||
|
||
It can also be used as a standalone JWT generator for use with Vonage APIs, like so: | ||
|
||
### Import the `JwtClient` object | ||
|
||
```python | ||
from vonage_jwt.jwt import JwtClient | ||
``` | ||
|
||
### Create a `JwtClient` object | ||
|
||
```python | ||
jwt_client = JwtClient(application_id, private_key) | ||
``` | ||
|
||
### Generate a JWT using the provided application id and private key | ||
|
||
```python | ||
jwt_client.generate_application_jwt() | ||
``` | ||
|
||
Optional JWT claims can be provided in a python dictionary: | ||
|
||
```python | ||
claims = {'jti': 'asdfzxcv1234', 'nbf': now + 100} | ||
jwt_client.generate_application_jwt(claims) | ||
``` | ||
|
||
## Verifying a JWT signature | ||
|
||
You can use the `verify_jwt.verify_signature` method to verify a JWT signature is valid. | ||
|
||
```python | ||
from vonage_jwt.verify_jwt import verify_signature | ||
|
||
verify_signature(TOKEN, SIGNATURE_SECRET) # Returns a boolean | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
[project] | ||
name = "vonage-jwt" | ||
version = "1.1.1" | ||
description = "Tooling for working with JWTs for Vonage APIs in Python." | ||
readme = "README.md" | ||
authors = [{ name = "Vonage", email = "devrel@vonage.com" }] | ||
requires-python = ">=3.8" | ||
dependencies = [ | ||
"pyjwt[crypto] >=1.6.4" | ||
] | ||
classifiers = [ | ||
"Programming Language :: Python", | ||
"Programming Language :: Python :: 3", | ||
"Programming Language :: Python :: 3.8", | ||
"Programming Language :: Python :: 3.9", | ||
"Programming Language :: Python :: 3.10", | ||
"Programming Language :: Python :: 3.11", | ||
"Programming Language :: Python :: 3.12", | ||
"License :: OSI Approved :: Apache Software License", | ||
] | ||
|
||
[project.urls] | ||
Homepage = "https://github.com/Vonage/vonage-python-sdk" | ||
|
||
[build-system] | ||
requires = ["setuptools>=61.0", "wheel"] | ||
build-backend = "setuptools.build_meta" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
python_sources() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from .jwt import JwtClient, VonageJwtError | ||
from .verify_jwt import verify_signature | ||
|
||
__all__ = ['JwtClient', 'VonageJwtError', 'verify_signature'] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import re | ||
from time import time | ||
from jwt import encode | ||
from uuid import uuid4 | ||
from typing import Union | ||
|
||
|
||
class JwtClient: | ||
"""Object used to pass in an application ID and private key to generate JWT methods.""" | ||
|
||
def __init__(self, application_id: str, private_key: str): | ||
self._application_id = application_id | ||
|
||
try: | ||
self._set_private_key(private_key) | ||
except Exception as err: | ||
raise VonageJwtError(err) | ||
|
||
if self._application_id is None or self._private_key is None: | ||
raise VonageJwtError( | ||
'Both of "application_id" and "private_key" are required.' | ||
) | ||
|
||
def generate_application_jwt(self, jwt_options: dict = {}): | ||
""" | ||
Generates a JWT for the specified Vonage application. | ||
You can override values for application_id and private_key on the JWTClient object by | ||
specifying them in the `jwt_options` dict if required. | ||
""" | ||
|
||
iat = int(time()) | ||
|
||
payload = jwt_options | ||
payload["application_id"] = self._application_id | ||
payload.setdefault("iat", iat) | ||
payload.setdefault("jti", str(uuid4())) | ||
payload.setdefault("exp", iat + (15 * 60)) | ||
|
||
headers = {'alg': 'RS256', 'typ': 'JWT'} | ||
|
||
token = encode(payload, self._private_key, algorithm='RS256', headers=headers) | ||
return bytes(token, 'utf-8') | ||
|
||
def _set_private_key(self, key: Union[str, bytes]): | ||
if isinstance(key, (str, bytes)) and re.search("[.][a-zA-Z0-9_]+$", key): | ||
with open(key, "rb") as key_file: | ||
self._private_key = key_file.read() | ||
elif isinstance(key, str) and '-----BEGIN PRIVATE KEY-----' not in key: | ||
raise VonageJwtError( | ||
"If passing the private key directly as a string, it must be formatted correctly with newlines." | ||
) | ||
else: | ||
self._private_key = key | ||
|
||
|
||
class VonageJwtError(Exception): | ||
"""An error relating to the Vonage JWT Generator.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
from jwt import InvalidSignatureError, decode | ||
|
||
|
||
def verify_signature(token: str, signature_secret: str = None) -> bool: | ||
""" | ||
Method to verify that an incoming JWT was sent by Vonage. | ||
""" | ||
|
||
try: | ||
decode(token, signature_secret, algorithms='HS256') | ||
return True | ||
except InvalidSignatureError: | ||
return False | ||
except Exception as e: | ||
raise VonageVerifyJwtError(repr(e)) | ||
|
||
|
||
class VonageVerifyJwtError(Exception): | ||
"""The signature could not be verified.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
python_tests(dependencies=['messages', 'testutils']) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
-----BEGIN PRIVATE KEY----- | ||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQdAHqJHs/a+Ra | ||
2ubvSd1vz/aWlJ9BqnMUtB7guTlyggdENAbleIkzep6mUHepDJdQh8Qv6zS3lpUe | ||
K0UkDfr1/FvsvxurGw/YYPagUEhP/HxMbs2rnQTiAdWOT+Ux9vPABoyNYvZB90xN | ||
IVhBDRWgkz1HPQBRNjFcm3NOol83h5Uwp5YroGTWx+rpmIiRhQj3mv6luk102d95 | ||
4ulpPpzcYWKIpJNdclJrEkBZaghDZTOpbv79qd+ds9AVp1j8i9cG/owBJpsJWxfw | ||
StMDpNeEZqopeQWmA121sSEsxpAbKJ5DA7F/lmckx74sulKHX1fDWT76cRhloaEQ | ||
VmETdj0VAgMBAAECggEAZ+SBtchz8vKbsBqtAbM/XcR5Iqi1TR2eWMHDJ/65HpSm | ||
+XuyujjerN0e6EZvtT4Uxmq8QaPJNP0kmhI31hXvsB0UVcUUDa4hshb1pIYO3Gq7 | ||
Kr8I29EZB2mhndm9Ii9yYhEBiVA66zrNeR225kkWr97iqjhBibhoVr8Vc6oiqcIP | ||
nFy5zSFtQSkhucaPge6rW00JSOD3wg2GM+rgS6r22t8YmqTzAwvwfil5pQfUngal | ||
oywqLOf6CUYXPBleJc1KgaIIP/cSvqh6b/t25o2VXnI4rpRhtleORvYBbH6K6xLa | ||
OWgg6B58T+0/QEqtZIAn4miYtVCkYLB78Ormc7Q9ewKBgQDuSytuYqxdZh/L/RDU | ||
CErFcNO5I1e9fkLAs5dQEBvvdQC74+oA1MsDEVv0xehFa1JwPKSepmvB2UznZg9L | ||
CtR7QKMDZWvS5xx4j0E/b+PiNQ/tlcFZB2UZ0JwviSxdd7omOTscq9c3RIhFHar1 | ||
Y38Fixkfm44Ij/K3JqIi2v2QMwKBgQDf8TYOOmAr9UuipUDxMsRSqTGVIY8B+aEJ | ||
W+2aLrqJVkLGTRfrbjzXWYo3+n7kNJjFgNkltDq6HYtufHMYRs/0PPtNR0w0cDPS | ||
Xr7m2LNHTDcBalC/AS4yKZJLNLm+kXA84vkw4qiTjc0LSFxJkouTQzkea0l8EWHt | ||
zRMv/qYVlwKBgBaJOWRJJK/4lo0+M7c5yYh+sSdTNlsPc9Sxp1/FBj9RO26JkXne | ||
pgx2OdIeXWcjTTqcIZ13c71zhZhkyJF6RroZVNFfaCEcBk9IjQ0o0c504jq/7Pc0 | ||
gdU9K2g7etykFBDFXNfLUKFDc/fFZIOskzi8/PVGStp4cqXrm23cdBqNAoGBAKtf | ||
A2bP9ViuVjsZCyGJIAPBxlfBXpa8WSe4WZNrvwPqJx9pT6yyp4yE0OkVoJUyStaZ | ||
S5M24NocUd8zDUC+r9TP9d+leAOI+Z87MgumOUuOX2mN2kzQsnFgrrsulhXnZmSx | ||
rNBkI20HTqobrcP/iSAgiU1l/M4c3zwDe3N3A9HxAoGBAM2hYu0Ij6htSNgo/WWr | ||
IEYYXuwf8hPkiuwzlaiWhD3eocgd4S8SsBu/bTCY19hQ2QbBPaYyFlNem+ynQyXx | ||
IOacrgIHCrYnRCxjPfFF/MxgUHJb8ZoiexprP/FME5p0PoRQIEFYa+jVht3hT5wC | ||
9aedWufq4JJb+akO6MVUjTvs | ||
-----END PRIVATE KEY----- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
-----BEGIN PUBLIC KEY----- | ||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0HQB6iR7P2vkWtrm70nd | ||
b8/2lpSfQapzFLQe4Lk5coIHRDQG5XiJM3qeplB3qQyXUIfEL+s0t5aVHitFJA36 | ||
9fxb7L8bqxsP2GD2oFBIT/x8TG7Nq50E4gHVjk/lMfbzwAaMjWL2QfdMTSFYQQ0V | ||
oJM9Rz0AUTYxXJtzTqJfN4eVMKeWK6Bk1sfq6ZiIkYUI95r+pbpNdNnfeeLpaT6c | ||
3GFiiKSTXXJSaxJAWWoIQ2UzqW7+/anfnbPQFadY/IvXBv6MASabCVsX8ErTA6TX | ||
hGaqKXkFpgNdtbEhLMaQGyieQwOxf5ZnJMe+LLpSh19Xw1k++nEYZaGhEFZhE3Y9 | ||
FQIDAQAB | ||
-----END PUBLIC KEY----- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
from vonage_jwt.jwt import JwtClient, VonageJwtError | ||
|
||
import os | ||
from pytest import raises | ||
from jwt import decode | ||
from jwt.exceptions import ImmatureSignatureError | ||
from time import time | ||
|
||
# Ensure the client isn't being configured with real values | ||
os.environ.clear() | ||
|
||
|
||
def read_file(path): | ||
with open(os.path.join(os.path.dirname(__file__), path)) as input_file: | ||
return input_file.read() | ||
|
||
|
||
application_id = 'asdf1234' | ||
private_key_string = read_file('data/private_key.txt') | ||
private_key_file_path = './tests/data/private_key.txt' | ||
jwt_client = JwtClient(application_id, private_key_file_path) | ||
|
||
public_key = read_file('data/public_key.txt') | ||
|
||
|
||
def test_create_jwt_client_key_string(): | ||
jwt_client = JwtClient(application_id, private_key_string) | ||
assert jwt_client._application_id == application_id | ||
assert jwt_client._private_key == private_key_string | ||
|
||
|
||
def test_create_jwt_client_key_file(): | ||
jwt_client = JwtClient(application_id, private_key_file_path) | ||
assert jwt_client._application_id == application_id | ||
assert jwt_client._private_key == bytes(private_key_string, 'utf-8') | ||
|
||
|
||
def test_create_jwt_client_error_incomplete(): | ||
with raises(VonageJwtError) as err: | ||
JwtClient(application_id, None) | ||
assert str(err.value) == 'Both of "application_id" and "private_key" are required.' | ||
|
||
|
||
def test_create_jwt_client_error_invalid_key(): | ||
with raises(VonageJwtError) as err: | ||
JwtClient(application_id, 'invalid-private-key-string') | ||
assert ( | ||
str(err.value) | ||
== 'If passing the private key directly as a string, it must be formatted correctly with newlines.' | ||
) | ||
|
||
|
||
def test_generate_application_jwt_basic(): | ||
jwt = jwt_client.generate_application_jwt() | ||
decoded_jwt = decode(jwt, key=public_key, algorithms='RS256') | ||
assert decoded_jwt['application_id'] == 'asdf1234' | ||
assert decoded_jwt['exp'] - decoded_jwt['iat'] == 15 * 60 | ||
|
||
|
||
def test_generate_application_jwt_custom_claims(): | ||
now = int(time()) | ||
claims = {'jti': 'qwerasdfzxcv1234', 'nbf': now + 100} | ||
jwt = jwt_client.generate_application_jwt(claims) | ||
with raises(ImmatureSignatureError) as err: | ||
decode(jwt, key=public_key, algorithms='RS256') | ||
assert str(err.value) == 'The token is not yet valid (nbf)' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from vonage_jwt.verify_jwt import verify_signature, VonageVerifyJwtError | ||
import pytest | ||
|
||
token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2OTc2MzQ2ODAsImV4cCI6MzMyNTQ1NDA4MjgsImF1ZCI6IiIsInN1YiI6IiJ9.88vJc3I2HhuqEDixHXVhc9R30tA6U_HQHZTC29y6CGM' | ||
valid_signature = "qwertyuiopasdfghjklzxcvbnm123456" | ||
invalid_signature = 'asdf' | ||
|
||
|
||
def test_verify_signature_valid(): | ||
assert verify_signature(token, valid_signature) is True | ||
|
||
|
||
def test_verify_signature_invalid(): | ||
assert verify_signature(token, invalid_signature) is False | ||
|
||
|
||
def test_verify_signature_error(): | ||
with pytest.raises(VonageVerifyJwtError) as e: | ||
verify_signature('asdf', valid_signature) | ||
assert 'DecodeError' in str(e.value) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters