Skip to content

Commit 445cab8

Browse files
committed
start migration of jwt module
1 parent 27bcffa commit 445cab8

File tree

14 files changed

+300
-2
lines changed

14 files changed

+300
-2
lines changed

http_client/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors = [{ name = "Vonage", email = "[email protected]" }]
77
requires-python = ">=3.8"
88
dependencies = [
99
"vonage-utils>=1.1.1",
10-
"vonage-jwt>=1.1.0",
10+
"vonage-jwt>=1.1.1",
1111
"requests>=2.27.0",
1212
"typing-extensions>=4.9.0",
1313
"pydantic>=2.7.1",

jwt/CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# 1.1.0
2+
- Add new module with method to verify JWT signatures, `verify_jwt.verify_signature`
3+
4+
# 1.0.0
5+
- First stable release

jwt/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Vonage JWT Generator for Python
2+
3+
This package (`vonage-jwt`) provides functionality to generate a JWT in Python code.
4+
5+
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.
6+
7+
For full API documentation, refer to the [Vonage Developer documentation](https://developer.vonage.com).
8+
9+
- [Installation](#installation)
10+
- [Generating JWTs](#generating-jwts)
11+
- [Verifying a JWT signature](#verifying-a-jwt-signature)
12+
13+
## Installation
14+
15+
Install from the Python Package Index with pip:
16+
17+
```bash
18+
pip install vonage-jwt
19+
```
20+
21+
## Generating JWTs
22+
23+
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.
24+
25+
It can also be used as a standalone JWT generator for use with Vonage APIs, like so:
26+
27+
### Import the `JwtClient` object
28+
29+
```python
30+
from vonage_jwt.jwt import JwtClient
31+
```
32+
33+
### Create a `JwtClient` object
34+
35+
```python
36+
jwt_client = JwtClient(application_id, private_key)
37+
```
38+
39+
### Generate a JWT using the provided application id and private key
40+
41+
```python
42+
jwt_client.generate_application_jwt()
43+
```
44+
45+
Optional JWT claims can be provided in a python dictionary:
46+
47+
```python
48+
claims = {'jti': 'asdfzxcv1234', 'nbf': now + 100}
49+
jwt_client.generate_application_jwt(claims)
50+
```
51+
52+
## Verifying a JWT signature
53+
54+
You can use the `verify_jwt.verify_signature` method to verify a JWT signature is valid.
55+
56+
```python
57+
from vonage_jwt.verify_jwt import verify_signature
58+
59+
verify_signature(TOKEN, SIGNATURE_SECRET) # Returns a boolean
60+
```

jwt/pyproject.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[project]
2+
name = "vonage-jwt"
3+
version = "1.1.1"
4+
description = "Tooling for working with JWTs for Vonage APIs in Python."
5+
readme = "README.md"
6+
authors = [{ name = "Vonage", email = "[email protected]" }]
7+
requires-python = ">=3.8"
8+
dependencies = [
9+
"pyjwt[crypto] >=1.6.4"
10+
]
11+
classifiers = [
12+
"Programming Language :: Python",
13+
"Programming Language :: Python :: 3",
14+
"Programming Language :: Python :: 3.8",
15+
"Programming Language :: Python :: 3.9",
16+
"Programming Language :: Python :: 3.10",
17+
"Programming Language :: Python :: 3.11",
18+
"Programming Language :: Python :: 3.12",
19+
"License :: OSI Approved :: Apache Software License",
20+
]
21+
22+
[project.urls]
23+
Homepage = "https://github.com/Vonage/vonage-python-sdk"
24+
25+
[build-system]
26+
requires = ["setuptools>=61.0", "wheel"]
27+
build-backend = "setuptools.build_meta"

jwt/src/vonage_jwt/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python_sources()

jwt/src/vonage_jwt/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .jwt import JwtClient, VonageJwtError
2+
from .verify_jwt import verify_signature
3+
4+
__all__ = ['JwtClient', 'VonageJwtError', 'verify_signature']

jwt/src/vonage_jwt/jwt.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import re
2+
from time import time
3+
from jwt import encode
4+
from uuid import uuid4
5+
from typing import Union
6+
7+
8+
class JwtClient:
9+
"""Object used to pass in an application ID and private key to generate JWT methods."""
10+
11+
def __init__(self, application_id: str, private_key: str):
12+
self._application_id = application_id
13+
14+
try:
15+
self._set_private_key(private_key)
16+
except Exception as err:
17+
raise VonageJwtError(err)
18+
19+
if self._application_id is None or self._private_key is None:
20+
raise VonageJwtError(
21+
'Both of "application_id" and "private_key" are required.'
22+
)
23+
24+
def generate_application_jwt(self, jwt_options: dict = {}):
25+
"""
26+
Generates a JWT for the specified Vonage application.
27+
You can override values for application_id and private_key on the JWTClient object by
28+
specifying them in the `jwt_options` dict if required.
29+
"""
30+
31+
iat = int(time())
32+
33+
payload = jwt_options
34+
payload["application_id"] = self._application_id
35+
payload.setdefault("iat", iat)
36+
payload.setdefault("jti", str(uuid4()))
37+
payload.setdefault("exp", iat + (15 * 60))
38+
39+
headers = {'alg': 'RS256', 'typ': 'JWT'}
40+
41+
token = encode(payload, self._private_key, algorithm='RS256', headers=headers)
42+
return bytes(token, 'utf-8')
43+
44+
def _set_private_key(self, key: Union[str, bytes]):
45+
if isinstance(key, (str, bytes)) and re.search("[.][a-zA-Z0-9_]+$", key):
46+
with open(key, "rb") as key_file:
47+
self._private_key = key_file.read()
48+
elif isinstance(key, str) and '-----BEGIN PRIVATE KEY-----' not in key:
49+
raise VonageJwtError(
50+
"If passing the private key directly as a string, it must be formatted correctly with newlines."
51+
)
52+
else:
53+
self._private_key = key
54+
55+
56+
class VonageJwtError(Exception):
57+
"""An error relating to the Vonage JWT Generator."""

jwt/src/vonage_jwt/verify_jwt.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from jwt import InvalidSignatureError, decode
2+
3+
4+
def verify_signature(token: str, signature_secret: str = None) -> bool:
5+
"""
6+
Method to verify that an incoming JWT was sent by Vonage.
7+
"""
8+
9+
try:
10+
decode(token, signature_secret, algorithms='HS256')
11+
return True
12+
except InvalidSignatureError:
13+
return False
14+
except Exception as e:
15+
raise VonageVerifyJwtError(repr(e))
16+
17+
18+
class VonageVerifyJwtError(Exception):
19+
"""The signature could not be verified."""

jwt/tests/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python_tests(dependencies=['messages', 'testutils'])

jwt/tests/data/private_key.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQdAHqJHs/a+Ra
3+
2ubvSd1vz/aWlJ9BqnMUtB7guTlyggdENAbleIkzep6mUHepDJdQh8Qv6zS3lpUe
4+
K0UkDfr1/FvsvxurGw/YYPagUEhP/HxMbs2rnQTiAdWOT+Ux9vPABoyNYvZB90xN
5+
IVhBDRWgkz1HPQBRNjFcm3NOol83h5Uwp5YroGTWx+rpmIiRhQj3mv6luk102d95
6+
4ulpPpzcYWKIpJNdclJrEkBZaghDZTOpbv79qd+ds9AVp1j8i9cG/owBJpsJWxfw
7+
StMDpNeEZqopeQWmA121sSEsxpAbKJ5DA7F/lmckx74sulKHX1fDWT76cRhloaEQ
8+
VmETdj0VAgMBAAECggEAZ+SBtchz8vKbsBqtAbM/XcR5Iqi1TR2eWMHDJ/65HpSm
9+
+XuyujjerN0e6EZvtT4Uxmq8QaPJNP0kmhI31hXvsB0UVcUUDa4hshb1pIYO3Gq7
10+
Kr8I29EZB2mhndm9Ii9yYhEBiVA66zrNeR225kkWr97iqjhBibhoVr8Vc6oiqcIP
11+
nFy5zSFtQSkhucaPge6rW00JSOD3wg2GM+rgS6r22t8YmqTzAwvwfil5pQfUngal
12+
oywqLOf6CUYXPBleJc1KgaIIP/cSvqh6b/t25o2VXnI4rpRhtleORvYBbH6K6xLa
13+
OWgg6B58T+0/QEqtZIAn4miYtVCkYLB78Ormc7Q9ewKBgQDuSytuYqxdZh/L/RDU
14+
CErFcNO5I1e9fkLAs5dQEBvvdQC74+oA1MsDEVv0xehFa1JwPKSepmvB2UznZg9L
15+
CtR7QKMDZWvS5xx4j0E/b+PiNQ/tlcFZB2UZ0JwviSxdd7omOTscq9c3RIhFHar1
16+
Y38Fixkfm44Ij/K3JqIi2v2QMwKBgQDf8TYOOmAr9UuipUDxMsRSqTGVIY8B+aEJ
17+
W+2aLrqJVkLGTRfrbjzXWYo3+n7kNJjFgNkltDq6HYtufHMYRs/0PPtNR0w0cDPS
18+
Xr7m2LNHTDcBalC/AS4yKZJLNLm+kXA84vkw4qiTjc0LSFxJkouTQzkea0l8EWHt
19+
zRMv/qYVlwKBgBaJOWRJJK/4lo0+M7c5yYh+sSdTNlsPc9Sxp1/FBj9RO26JkXne
20+
pgx2OdIeXWcjTTqcIZ13c71zhZhkyJF6RroZVNFfaCEcBk9IjQ0o0c504jq/7Pc0
21+
gdU9K2g7etykFBDFXNfLUKFDc/fFZIOskzi8/PVGStp4cqXrm23cdBqNAoGBAKtf
22+
A2bP9ViuVjsZCyGJIAPBxlfBXpa8WSe4WZNrvwPqJx9pT6yyp4yE0OkVoJUyStaZ
23+
S5M24NocUd8zDUC+r9TP9d+leAOI+Z87MgumOUuOX2mN2kzQsnFgrrsulhXnZmSx
24+
rNBkI20HTqobrcP/iSAgiU1l/M4c3zwDe3N3A9HxAoGBAM2hYu0Ij6htSNgo/WWr
25+
IEYYXuwf8hPkiuwzlaiWhD3eocgd4S8SsBu/bTCY19hQ2QbBPaYyFlNem+ynQyXx
26+
IOacrgIHCrYnRCxjPfFF/MxgUHJb8ZoiexprP/FME5p0PoRQIEFYa+jVht3hT5wC
27+
9aedWufq4JJb+akO6MVUjTvs
28+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)